├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── config
└── haystack.php
├── database
├── factories
│ ├── HaystackBaleFactory.php
│ ├── HaystackDataFactory.php
│ └── HaystackFactory.php
└── migrations
│ ├── create_haystack_bales_table.php.stub
│ ├── create_haystack_data_table.php.stub
│ └── create_haystacks_table.php.stub
├── phpunit.xml
└── src
├── Builders
└── HaystackBuilder.php
├── Casts
├── CallbackCollectionCast.php
├── MiddlewareCollectionCast.php
├── SerializeClosure.php
├── Serialized.php
└── SerializedModel.php
├── ChunkableHaystackJob.php
├── Concerns
├── ManagesBales.php
└── Stackable.php
├── Console
└── Commands
│ ├── HaystackInstall.php
│ ├── HaystacksClear.php
│ ├── HaystacksForget.php
│ └── HaystacksResume.php
├── Contracts
└── StackableJob.php
├── Data
├── CallbackCollection.php
├── HaystackOptions.php
├── MiddlewareCollection.php
├── NextJob.php
├── PendingData.php
├── PendingHaystackBale.php
└── SerializedModel.php
├── Enums
└── FinishStatus.php
├── Exceptions
└── HaystackModelExists.php
├── HaystackServiceProvider.php
├── Helpers
├── CarbonHelper.php
├── ClosureHelper.php
├── DataHelper.php
├── DataValidator.php
├── ExceptionHelper.php
└── SerializationHelper.php
├── JobEventListener.php
├── Middleware
├── CheckAttempts.php
├── CheckFinished.php
└── IncrementAttempts.php
└── Models
├── Haystack.php
├── HaystackBale.php
└── HaystackData.php
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in([
5 | __DIR__,
6 | ])
7 | ->name('*.php')
8 | ->notName('*.blade.php')
9 | ->ignoreDotFiles(true)
10 | ->ignoreVCS(true)
11 | ->exclude(['vendor', 'node_modules']);
12 |
13 | $config = new PhpCsFixer\Config();
14 |
15 | // Rules from: https://cs.symfony.com/doc/rules/index.html
16 |
17 | return $config->setRules([
18 | '@PSR2' => true,
19 | 'array_syntax' => ['syntax' => 'short'],
20 | 'ordered_imports' => ['sort_algorithm' => 'length'],
21 | 'no_unused_imports' => true,
22 | 'not_operator_with_successor_space' => true,
23 | 'trailing_comma_in_multiline' => true,
24 | 'single_quote' => ['strings_containing_single_quote_chars' => true],
25 | 'phpdoc_scalar' => true,
26 | 'unary_operator_spaces' => true,
27 | 'binary_operator_spaces' => true,
28 | 'blank_line_before_statement' => [
29 | 'statements' => ['declare', 'return', 'throw', 'try'],
30 | ],
31 | 'phpdoc_single_line_var_spacing' => true,
32 | 'phpdoc_var_without_name' => true,
33 | 'method_argument_space' => [
34 | 'on_multiline' => 'ensure_fully_multiline',
35 | 'keep_multiple_spaces_after_comma' => true,
36 | ],
37 | 'return_type_declaration' => [
38 | 'space_before' => 'none'
39 | ],
40 | 'declare_strict_types' => true,
41 | 'blank_line_after_opening_tag' => true,
42 | 'single_import_per_statement' => true,
43 | 'mb_str_functions' => true,
44 | 'no_superfluous_phpdoc_tags' => true,
45 | 'no_blank_lines_after_phpdoc' => true,
46 | 'no_empty_phpdoc' => true,
47 | 'phpdoc_trim' => true,
48 | ])->setFinder($finder);
49 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to `laravel-haystack` will be documented in this file.
4 |
5 | ## Version v0.10.1 - 2022-10-30
6 |
7 | ### What's Changed
8 |
9 | - Fix typo command output on clearing haystacks by @faisuc in https://github.com/Sammyjo20/laravel-haystack/pull/58
10 |
11 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.10.0...v0.10.1
12 |
13 | ## Version v0.10.0 - 2022-10-07
14 |
15 | ### What's Changed
16 |
17 | - Feature | Multiple Callbacks & Middleware by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/48
18 |
19 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.9.0...v0.10.0
20 |
21 | ## Version v0.9.0 - 2022-10-04
22 |
23 | ### What's Changed
24 |
25 | - Fix | Removed Spatie Laravel Package Tools by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/47
26 |
27 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.8.5...v0.9.0
28 |
29 | ## Version v0.8.5 - 2022-10-04
30 |
31 | - Updated docs
32 |
33 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.8.4...v0.8.5
34 |
35 | ## Version v0.8.4 - 2022-10-01
36 |
37 | ### What's Changed
38 |
39 | - Feature | Configured install command by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/46
40 |
41 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.8.3...v0.8.4
42 |
43 | ## Version v0.8.3 - 2022-10-01
44 |
45 | ### What's Changed
46 |
47 | - Feature | Models on Haystacks by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/45
48 |
49 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.8.2...v0.8.3
50 |
51 | ## Version v0.8.2 - 2022-09-29
52 |
53 | ### What's Changed
54 |
55 | - Feature | Allow Failures Improvements by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/43
56 |
57 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.8.1...v0.8.2
58 |
59 | ## Version v0.8.1 - 2022-09-29
60 |
61 | - Revised documentation
62 |
63 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.8.0...v0.8.1
64 |
65 | ## Version v0.8.0 - 2022-09-29
66 |
67 | ### New Features
68 |
69 | - New `allowFailures` option added when building Haystacks to continue processing the next job even if jobs fail. @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/42
70 |
71 | ### Other Changes
72 |
73 | - Switched to a new HaystackOptions class instead of multiple database options for better compatibility with future options.
74 | - Fixed an issue where timestamps weren't being set when haystack bales are added to the queue
75 |
76 | ### Breaking Changes
77 |
78 | - Added a new text "options" column to store the new HaystackOptions class.
79 | - Removed old `return_data` column on the `haystacks` table
80 | - Changed `value` column on the `haystack_data` table to a longText to support more data.
81 |
82 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.7.6...v0.8.0
83 |
84 | ## Version v0.7.6 - 2022-09-18
85 |
86 | ### What's Changed
87 |
88 | - Feature | Chunkable Jobs by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/40
89 |
90 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.7.5...v0.7.6
91 |
92 | ## Version v0.7.5 - 2022-09-11
93 |
94 | ### What's Changed
95 |
96 | - Feature | Initial Data by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/38
97 |
98 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.7.4...v0.7.5
99 |
100 | ## Version v0.7.4 - 2022-08-19
101 |
102 | ### What's Changed
103 |
104 | - Use conditional clauses when building haystack by @faisuc in https://github.com/Sammyjo20/laravel-haystack/pull/32
105 |
106 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.7.3...v0.7.4
107 |
108 | ## Version v0.7.3 - 2022-08-18
109 |
110 | ### What's Changed
111 |
112 | - Feature | Added declare strict types by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/31
113 |
114 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.7.2...v0.7.3
115 |
116 | ## Version v0.7.2 - 2022-08-18
117 |
118 | ### What's Changed
119 |
120 | - Add conditional objects when adding job to haystack by @faisuc in https://github.com/Sammyjo20/laravel-haystack/pull/30
121 |
122 | ### New Contributors
123 |
124 | - @faisuc made their first contribution in https://github.com/Sammyjo20/laravel-haystack/pull/30
125 |
126 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.7.1...v0.7.2
127 |
128 | ## Version v0.7.1 - 2022-08-10
129 |
130 | ### What's Changed
131 |
132 | - Add `haystacks:clear` and `haystacks:forget` commands by @viicslen in https://github.com/Sammyjo20/laravel-haystack/pull/29
133 |
134 | ### New Contributors
135 |
136 | - @viicslen made their first contribution in https://github.com/Sammyjo20/laravel-haystack/pull/29
137 |
138 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.7.0...v0.7.1
139 |
140 | ## Version v0.7.0 - 2022-08-09
141 |
142 | ### What's Changed
143 |
144 | - Names and appending multiple jobs by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/26
145 |
146 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.6.0...v0.7.0
147 |
148 | ## Version v0.6.0 - 2022-08-08
149 |
150 | ### What's Changed
151 |
152 | - Changed "appendToHaystackNext" to "prependToHaystack" by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/25
153 |
154 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.5.0...v0.6.0
155 |
156 | ## Version v0.5.0 - 2022-08-08
157 |
158 | ### What's Changed
159 |
160 | - Append to the next job in the haystack by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/24
161 |
162 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.4.3...v0.5.0
163 |
164 | ## Version v0.4.3 - 2022-08-07
165 |
166 | ### What's Changed
167 |
168 | - Cancelling haystacks by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/20
169 |
170 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.4.2...v0.4.3
171 |
172 | ## Version v0.4.2 - 2022-08-06
173 |
174 | ### What's Changed
175 |
176 | - Added closure check before running queries by @Sammyjo20 in https://github.com/Sammyjo20/laravel-haystack/pull/19
177 |
178 | **Full Changelog**: https://github.com/Sammyjo20/laravel-haystack/compare/v0.4.1...v0.4.2
179 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Sammyjo20 <29132017+Sammyjo20@users.noreply.github.com>
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > ### Notice 14/05/2024
2 | >
3 | > I am no longer going to be accepting new features for Laravel Haystack. I intend to still ensure security fixes
4 | > are made, but I feel that the project is now complete. Additionally, I feel that Laravel's job batches and
5 | > chains in Laravel 10+ are a lot more powerful and you may not need Laravel Haystack in 2024.
6 |
7 |
8 |
9 |

10 |
11 | # Laravel Haystack
12 | ⚡️ Supercharged job chains for Laravel
13 |
14 | 
15 |
16 | [Click here to read the documentation](https://docs.laravel-haystack.dev)
17 |
18 |
19 |
20 | Laravel Haystack provides supercharged job chains for Laravel. It comes with powerful features like delaying jobs for as long as you like, applying middleware to every job, sharing data and models between jobs and even chunking jobs. Laravel Haystack supports every queue connection/worker out of the box. (Database, Redis/Horizon, SQS). It's great if you need to queue thousands of jobs in a chain or if you are looking for features that the original Bus chain doesn't provide.
21 |
22 | ```php
23 | $haystack = Haystack::build()
24 | ->addJob(new RecordPodcast)
25 | ->addJob(new ProcessPodcast)
26 | ->addJob(new PublishPodcast)
27 | ->then(function () {
28 | // Haystack completed
29 | })
30 | ->catch(function () {
31 | // Haystack failed
32 | })
33 | ->finally(function () {
34 | // Always run either on success or fail.
35 | })
36 | ->withMiddleware([
37 | // Middleware for every job
38 | ])
39 | ->withDelay(60)
40 | ->withModel($user)
41 | ->dispatch();
42 | ```
43 |
44 | #### But doesn't Laravel already have job chains?
45 |
46 | That's right! Laravel does have job chains but they have some disadvantages that you might want to think about.
47 |
48 | * They consume quite a lot of memory/data since the chain is stored inside the job. This is especially true if you are storing thousands of jobs.
49 | * They are volatile, meaning if you lose one job in the chain - you lose the whole chain.
50 | * They do not provide the `then`, `catch`, `finally` callable methods that batched jobs do.
51 | * 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.
52 | * You can't share data between jobs as there is no "state" across the chain
53 |
54 | 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.
55 |
56 | #### Laravel Haystack Features
57 |
58 | * Low memory consumption as one job is processed at a time and the chain is stored in the database
59 | * You can delay/release jobs for as long as you want since it will use the scheduler to restart a chain. Even if your queue driver is SQS!
60 | * It provides callback methods like `then`, `catch` and `finally`.
61 | * Global middleware that can be applied to every single job in the chain
62 | * You can store models and data that are shared with every job in the chain.
63 | * You can prepare a Haystack and dispatch it at a later time
64 |
65 | #### Use Cases
66 |
67 | * If you need to make hundreds or thousands of API calls in a row, can be combined with Spatie's Job Rate Limiter to keep track of delays and pause jobs when a rate limit is hit.
68 | * If you need to queue thousands of jobs in a chain at a time.
69 | * If you need to batch import rows of data - each row can be a haystack job (bale) and processed one at a time. While keeping important job information stored in the database.
70 | * If you need "release" times longer than 15 minutes if you are using Amazon SQS
71 |
72 | ## Installation
73 |
74 | You can install the package with Composer. **Laravel Haystack Requires Laravel 8+ and PHP 8.1**
75 |
76 | ```bash
77 | composer require sammyjo20/laravel-haystack
78 | ```
79 |
80 | Next, just run the installation command!
81 |
82 | ```bash
83 | php artisan haystack:install
84 | ```
85 |
86 | ## Documentation
87 |
88 | [Click here to read the documentation](https://docs.laravel-haystack.dev)
89 |
90 | ## Support Haystack's Development
91 | 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!
92 |
93 |
94 |
95 | Thank you for using Laravel Haystack ❤️
96 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sammyjo20/laravel-haystack",
3 | "description": "Supercharged job chains for Laravel",
4 | "license": "MIT",
5 | "keywords": [
6 | "Sammyjo20",
7 | "laravel-haystack"
8 | ],
9 | "authors": [
10 | {
11 | "name": "Sammyjo20",
12 | "email": "29132017+Sammyjo20@users.noreply.github.com",
13 | "role": "Developer"
14 | }
15 | ],
16 | "homepage": "https://github.com/sammyjo20/laravel-haystack",
17 | "require": {
18 | "php": "^8.1",
19 | "illuminate/console": "^10.0 || ^11.7",
20 | "illuminate/contracts": "^10.0 || ^11.7",
21 | "illuminate/database": "^10.0 || ^v11.7",
22 | "illuminate/queue": "^10.0 || ^11.7",
23 | "illuminate/support": "^10.0 || ^11.7",
24 | "laravel/serializable-closure": "^1.2"
25 | },
26 | "require-dev": {
27 | "friendsofphp/php-cs-fixer": "^3.5",
28 | "orchestra/testbench": "^8.0 || ^9.0",
29 | "pestphp/pest": "^v2.34",
30 | "pestphp/pest-plugin-laravel": "^1.2 || ^2.0",
31 | "sammyjo20/laravel-chunkable-jobs": "^1.1",
32 | "spatie/laravel-ray": "^1.26"
33 | },
34 | "suggest": {
35 | "sammyjo20/laravel-chunkable-jobs": "Allows you to use the job chunking feature"
36 | },
37 | "minimum-stability": "stable",
38 | "autoload": {
39 | "psr-4": {
40 | "Sammyjo20\\LaravelHaystack\\": "src",
41 | "Sammyjo20\\LaravelHaystack\\Database\\Factories\\": "database/factories"
42 | }
43 | },
44 | "autoload-dev": {
45 | "psr-4": {
46 | "Sammyjo20\\LaravelHaystack\\Tests\\": "tests"
47 | }
48 | },
49 | "config": {
50 | "allow-plugins": {
51 | "pestphp/pest-plugin": true
52 | },
53 | "sort-packages": true
54 | },
55 | "extra": {
56 | "laravel": {
57 | "providers": [
58 | "Sammyjo20\\LaravelHaystack\\HaystackServiceProvider"
59 | ]
60 | }
61 | },
62 | "scripts": {
63 | "post-autoload-dump": [
64 | "@php ./vendor/bin/testbench package:discover --ansi"
65 | ],
66 | "fix-code": [
67 | "./vendor/bin/php-cs-fixer fix --allow-risky=yes"
68 | ],
69 | "pstan": [
70 | "./vendor/bin/phpstan analyse"
71 | ],
72 | "test": [
73 | "./vendor/bin/pest"
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/config/haystack.php:
--------------------------------------------------------------------------------
1 | true,
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Queue Haystack Jobs Automatically
23 | |--------------------------------------------------------------------------
24 | |
25 | | This value if set to true, will instruct Laravel Haystack to listen
26 | | out for "Stackable" jobs and automatically queue them after each
27 | | job is processed. If this value is set to false, you will need
28 | | to call "$this->nextJob" inside your jobs manually.
29 | |
30 | */
31 |
32 | 'process_automatically' => true,
33 |
34 | /*
35 | |--------------------------------------------------------------------------
36 | | Stale Haystacks
37 | |--------------------------------------------------------------------------
38 | |
39 | | This value determines how long "stale" haystacks are kept for. These are
40 | | haystacks where the job that controlled them has failed without sending
41 | | the failure signal to laravel-haystack. This shouldn't happen if auto
42 | | processing has been turned on.
43 | |
44 | */
45 |
46 | 'keep_stale_haystacks_for_days' => 3,
47 |
48 | /*
49 | |--------------------------------------------------------------------------
50 | | Delete Finished Haystacks
51 | |--------------------------------------------------------------------------
52 | |
53 | | This value determines if laravel-haystack should automatically delete
54 | | haystacks when they have finished processing. If this value is set
55 | | to false, make sure to use the scheduled command to clean up
56 | | old finished haystacks.
57 | |
58 | */
59 |
60 | 'delete_finished_haystacks' => true,
61 |
62 | /*
63 | |--------------------------------------------------------------------------
64 | | Keep Finished Haystacks For Days
65 | |--------------------------------------------------------------------------
66 | |
67 | | This value determines how long finished haystacks will be retained for
68 | | this is only applicable if "deleted_finished_haystacks" has been disabled.
69 | |
70 | */
71 |
72 | 'keep_finished_haystacks_for_days' => 1,
73 |
74 | /*
75 | |--------------------------------------------------------------------------
76 | | Default Database Connection Name
77 | |--------------------------------------------------------------------------
78 | |
79 | | Here you may specify which database connection you
80 | | use to store haystack jobs.
81 | |
82 | */
83 |
84 | 'db_connection' => env(
85 | 'HAYSTACK_DB_CONNECTION',
86 | env('DB_CONNECTION', 'mysql')
87 | ),
88 | ];
89 |
--------------------------------------------------------------------------------
/database/factories/HaystackBaleFactory.php:
--------------------------------------------------------------------------------
1 | new HaystackOptions,
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/migrations/create_haystack_bales_table.php.stub:
--------------------------------------------------------------------------------
1 | id();
14 | $table->timestamps();
15 | $table->foreignIdFor(Haystack::class)->constrained()->cascadeOnDelete();
16 | $table->mediumText('job');
17 | $table->bigInteger('delay')->default(0);
18 | $table->string('on_queue')->nullable();
19 | $table->string('on_connection')->nullable();
20 | $table->integer('attempts')->default(0);
21 | $table->integer('retry_until')->nullable();
22 | $table->boolean('priority')->default(false);
23 |
24 | $table->index(['priority', 'id']);
25 | });
26 | }
27 |
28 | public function down()
29 | {
30 | Schema::dropIfExists('haystack_bales');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/database/migrations/create_haystack_data_table.php.stub:
--------------------------------------------------------------------------------
1 | id();
14 | $table->timestamps();
15 | $table->foreignIdFor(Haystack::class)->constrained()->cascadeOnDelete();
16 | $table->string('key');
17 | $table->longText('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 | };
29 |
--------------------------------------------------------------------------------
/database/migrations/create_haystacks_table.php.stub:
--------------------------------------------------------------------------------
1 | id();
13 | $table->string('name')->nullable();
14 | $table->mediumText('callbacks')->nullable();
15 | $table->mediumText('middleware')->nullable();
16 | $table->text('options');
17 | $table->timestamps();
18 | $table->dateTime('started_at')->nullable();
19 | $table->dateTime('resume_at')->nullable();
20 | $table->dateTime('finished_at')->nullable();
21 | });
22 | }
23 |
24 | public function down()
25 | {
26 | Schema::dropIfExists('haystacks');
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ./app
20 | ./src
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Builders/HaystackBuilder.php:
--------------------------------------------------------------------------------
1 | jobs = new Collection;
93 | $this->initialData = new Collection;
94 | $this->callbacks = new CallbackCollection;
95 | $this->options = new HaystackOptions;
96 | $this->middleware = new MiddlewareCollection;
97 | }
98 |
99 | /**
100 | * Specify a name for the haystack.
101 | *
102 | * @return $this
103 | */
104 | public function withName(string $name): static
105 | {
106 | $this->name = $name;
107 |
108 | return $this;
109 | }
110 |
111 | /**
112 | * Provide a closure that will run when the haystack is complete.
113 | *
114 | * @return $this
115 | *
116 | * @throws PhpVersionNotSupportedException
117 | */
118 | public function then(Closure|callable $closure): static
119 | {
120 | $this->callbacks->addThen($closure);
121 |
122 | return $this;
123 | }
124 |
125 | /**
126 | * Provide a closure that will run when the haystack fails.
127 | *
128 | * @return $this
129 | *
130 | * @throws PhpVersionNotSupportedException
131 | */
132 | public function catch(Closure|callable $closure): static
133 | {
134 | $this->callbacks->addCatch($closure);
135 |
136 | return $this;
137 | }
138 |
139 | /**
140 | * Provide a closure that will run when the haystack finishes.
141 | *
142 | * @return $this
143 | *
144 | * @throws PhpVersionNotSupportedException
145 | */
146 | public function finally(Closure|callable $closure): static
147 | {
148 | $this->callbacks->addFinally($closure);
149 |
150 | return $this;
151 | }
152 |
153 | /**
154 | * Provide a closure that will run when the haystack is paused.
155 | *
156 | * @return $this
157 | *
158 | * @throws PhpVersionNotSupportedException
159 | */
160 | public function paused(Closure|callable $closure): static
161 | {
162 | $this->callbacks->addPaused($closure);
163 |
164 | return $this;
165 | }
166 |
167 | /**
168 | * Add a job to the haystack.
169 | *
170 | * @return $this
171 | */
172 | public function addJob(StackableJob $job, int $delayInSeconds = 0, ?string $queue = null, ?string $connection = null): static
173 | {
174 | $pendingHaystackRow = new PendingHaystackBale($job, $delayInSeconds, $queue, $connection);
175 |
176 | $this->jobs->add($pendingHaystackRow);
177 |
178 | return $this;
179 | }
180 |
181 | /**
182 | * Add a job when a condition is true.
183 | *
184 | * @return $this
185 | */
186 | public function addJobWhen(bool $condition, ...$arguments): static
187 | {
188 | return $condition === true ? $this->addJob(...$arguments) : $this;
189 | }
190 |
191 | /**
192 | * Add a job when a condition is false.
193 | *
194 | * @return $this
195 | */
196 | public function addJobUnless(bool $condition, ...$arguments): static
197 | {
198 | return $this->addJobWhen(! $condition, ...$arguments);
199 | }
200 |
201 | /**
202 | * Add multiple jobs to the haystack at a time.
203 | *
204 | * @return $this
205 | */
206 | public function addJobs(Collection|array $jobs, int $delayInSeconds = 0, ?string $queue = null, ?string $connection = null): static
207 | {
208 | if (is_array($jobs)) {
209 | $jobs = collect($jobs);
210 | }
211 |
212 | $jobs = $jobs->filter(fn ($job) => $job instanceof StackableJob);
213 |
214 | foreach ($jobs as $job) {
215 | $this->addJob($job, $delayInSeconds, $queue, $connection);
216 | }
217 |
218 | return $this;
219 | }
220 |
221 | /**
222 | * Add jobs when a condition is true.
223 | *
224 | * @return $this
225 | */
226 | public function addJobsWhen(bool $condition, ...$arguments): static
227 | {
228 | return $condition === true ? $this->addJobs(...$arguments) : $this;
229 | }
230 |
231 | /**
232 | * Add jobs when a condition is false.
233 | *
234 | * @return $this
235 | */
236 | public function addJobsUnless(bool $condition, ...$arguments): static
237 | {
238 | return $this->addJobsWhen(! $condition, ...$arguments);
239 | }
240 |
241 | /**
242 | * Add a bale onto the haystack. Yee-haw!
243 | *
244 | * @alias addJob()
245 | *
246 | * @return $this
247 | */
248 | public function addBale(StackableJob $job, int $delayInSeconds = 0, ?string $queue = null, ?string $connection = null): static
249 | {
250 | return $this->addJob($job, $delayInSeconds, $queue, $connection);
251 | }
252 |
253 | /**
254 | * Add multiple bales onto the haystack. Yee-haw!
255 | *
256 | * @alias addJobs()
257 | *
258 | * @return $this
259 | */
260 | public function addBales(Collection|array $jobs, int $delayInSeconds = 0, ?string $queue = null, ?string $connection = null): static
261 | {
262 | return $this->addJobs($jobs, $delayInSeconds, $queue, $connection);
263 | }
264 |
265 | /**
266 | * Set a global delay on the jobs.
267 | *
268 | * @return $this
269 | */
270 | public function withDelay(int $seconds): static
271 | {
272 | $this->globalDelayInSeconds = $seconds;
273 |
274 | return $this;
275 | }
276 |
277 | public function pausedUntil(DateTimeInterface $resumeAt): static
278 | {
279 | if (! $resumeAt instanceof CarbonImmutable) {
280 | $resumeAt = Carbon::parse($resumeAt)->toImmutable();
281 | }
282 |
283 | $this->resumeAt = $resumeAt;
284 |
285 | return $this;
286 | }
287 |
288 | /**
289 | * Set a global queue for the jobs.
290 | *
291 | * @return $this
292 | */
293 | public function onQueue(string $queue): static
294 | {
295 | $this->globalQueue = $queue;
296 |
297 | return $this;
298 | }
299 |
300 | /**
301 | * Set a global connection for the jobs.
302 | *
303 | * @return $this
304 | */
305 | public function onConnection(string $connection): static
306 | {
307 | $this->globalConnection = $connection;
308 |
309 | return $this;
310 | }
311 |
312 | /**
313 | * Add some middleware to be merged in with every job
314 | *
315 | * @return $this
316 | *
317 | * @throws PhpVersionNotSupportedException
318 | */
319 | public function addMiddleware(Closure|callable|array $value): static
320 | {
321 | $this->middleware->add($value);
322 |
323 | return $this;
324 | }
325 |
326 | /**
327 | * Provide data before the haystack is created.
328 | *
329 | * @return $this
330 | */
331 | public function withData(string $key, mixed $value, ?string $cast = null): static
332 | {
333 | DataValidator::validateCast($value, $cast);
334 |
335 | $this->initialData->put($key, new PendingData($key, $value, $cast));
336 |
337 | return $this;
338 | }
339 |
340 | /**
341 | * Store a model to be shared across all haystack jobs.
342 | *
343 | * @return $this
344 | *
345 | * @throws HaystackModelExists
346 | */
347 | public function withModel(Model $model, ?string $key = null): static
348 | {
349 | $key = DataHelper::getModelKey($model, $key);
350 |
351 | if ($this->initialData->has($key)) {
352 | throw new HaystackModelExists($key);
353 | }
354 |
355 | $this->initialData->put($key, new PendingData($key, $model, SerializedModel::class));
356 |
357 | return $this;
358 | }
359 |
360 | /**
361 | * Create the Haystack
362 | */
363 | public function create(): Haystack
364 | {
365 | return DB::transaction(fn () => $this->createHaystack());
366 | }
367 |
368 | /**
369 | * Dispatch the Haystack.
370 | */
371 | public function dispatch(): Haystack
372 | {
373 | $haystack = $this->create();
374 |
375 | $haystack->start();
376 |
377 | return $haystack;
378 | }
379 |
380 | /**
381 | * Map the jobs to be ready for inserting.
382 | */
383 | protected function prepareJobsForInsert(Haystack $haystack): array
384 | {
385 | $now = Carbon::now();
386 |
387 | $timestamps = [
388 | 'created_at' => $now,
389 | 'updated_at' => $now,
390 | ];
391 |
392 | return $this->jobs->map(function (PendingHaystackBale $pendingJob) use ($haystack, $timestamps) {
393 | $hasDelay = isset($pendingJob->delayInSeconds) && $pendingJob->delayInSeconds > 0;
394 |
395 | // We'll create a dummy Haystack bale model for each row
396 | // and convert it into its attributes just for the casting.
397 |
398 | $baseAttributes = $haystack->bales()->make([
399 | 'job' => $pendingJob->job,
400 | 'delay' => $hasDelay ? $pendingJob->delayInSeconds : $this->globalDelayInSeconds,
401 | 'on_queue' => $pendingJob->queue ?? $this->globalQueue,
402 | 'on_connection' => $pendingJob->connection ?? $this->globalConnection,
403 | ])->getAttributes();
404 |
405 | // Next we'll merge in the timestamps
406 |
407 | return array_merge($timestamps, $baseAttributes);
408 | })->toArray();
409 | }
410 |
411 | /**
412 | * Map the initial data to be ready for inserting.
413 | */
414 | protected function prepareDataForInsert(Haystack $haystack): array
415 | {
416 | return $this->initialData->map(function (PendingData $pendingData) use ($haystack) {
417 | // We'll create a dummy Haystack data model for each row
418 | // and convert it into its attributes just for the casting.
419 |
420 | return $haystack->data()->make([
421 | 'key' => $pendingData->key,
422 | 'cast' => $pendingData->cast,
423 | 'value' => $pendingData->value,
424 | ])->getAttributes();
425 | })->toArray();
426 | }
427 |
428 | /**
429 | * Create the haystack.
430 | */
431 | protected function createHaystack(): Haystack
432 | {
433 | $haystack = new Haystack;
434 | $haystack->name = $this->name;
435 | $haystack->callbacks = $this->callbacks->toSerializable();
436 | $haystack->middleware = $this->middleware->toSerializable();
437 | $haystack->options = $this->options;
438 | $haystack->resume_at = $this->resumeAt;
439 |
440 | if ($this->beforeSave instanceof Closure) {
441 | $haystack = tap($haystack, $this->beforeSave);
442 | }
443 |
444 | $haystack->save();
445 |
446 | // We'll bulk insert the jobs and the data for efficient querying.
447 |
448 | if ($this->jobs->isNotEmpty()) {
449 | $haystack->bales()->insert($this->prepareJobsForInsert($haystack));
450 | }
451 |
452 | if ($this->initialData->isNotEmpty()) {
453 | $haystack->data()->insert($this->prepareDataForInsert($haystack));
454 | }
455 |
456 | return $haystack;
457 | }
458 |
459 | /**
460 | * Specify if you do not want haystack to return the data.
461 | *
462 | * @return $this
463 | */
464 | public function dontReturnData(): static
465 | {
466 | $this->options->returnDataOnFinish = false;
467 |
468 | return $this;
469 | }
470 |
471 | /**
472 | * Allow failures on the Haystack
473 | *
474 | * @return $this
475 | */
476 | public function allowFailures(): static
477 | {
478 | $this->options->allowFailures = true;
479 |
480 | return $this;
481 | }
482 |
483 | /**
484 | * Get all the jobs in the builder.
485 | */
486 | public function getJobs(): Collection
487 | {
488 | return $this->jobs;
489 | }
490 |
491 | /**
492 | * Retrieve the callbacks
493 | */
494 | public function getCallbacks(): CallbackCollection
495 | {
496 | return $this->callbacks;
497 | }
498 |
499 | /**
500 | * Get the time for the "withDelay".
501 | */
502 | public function getGlobalDelayInSeconds(): int
503 | {
504 | return $this->globalDelayInSeconds;
505 | }
506 |
507 | /**
508 | * Get the global queue
509 | */
510 | public function getGlobalQueue(): ?string
511 | {
512 | return $this->globalQueue;
513 | }
514 |
515 | /**
516 | * Get the global connection.
517 | */
518 | public function getGlobalConnection(): ?string
519 | {
520 | return $this->globalConnection;
521 | }
522 |
523 | /**
524 | * Get the closure for the global middleware.
525 | */
526 | public function getMiddleware(): MiddlewareCollection
527 | {
528 | return $this->middleware;
529 | }
530 |
531 | /**
532 | * Specify a closure to run before saving the Haystack
533 | *
534 | * @return $this
535 | */
536 | public function beforeSave(Closure $closure): static
537 | {
538 | $this->beforeSave = $closure;
539 |
540 | return $this;
541 | }
542 |
543 | /**
544 | * Set an option on the Haystack Options.
545 | *
546 | * @return $this
547 | */
548 | public function setOption(string $option, mixed $value): static
549 | {
550 | $this->options->$option = $value;
551 |
552 | return $this;
553 | }
554 | }
555 |
--------------------------------------------------------------------------------
/src/Casts/CallbackCollectionCast.php:
--------------------------------------------------------------------------------
1 | true]) : null;
22 | }
23 |
24 | /**
25 | * Serialize a job.
26 | *
27 | * @return mixed|string|null
28 | */
29 | public function set($model, string $key, $value, array $attributes)
30 | {
31 | if (blank($value)) {
32 | return null;
33 | }
34 |
35 | if (! $value instanceof CallbackCollection) {
36 | throw new InvalidArgumentException(sprintf('Value provided must be an instance of %s.', CallbackCollection::class));
37 | }
38 |
39 | return SerializationHelper::serialize($value);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Casts/MiddlewareCollectionCast.php:
--------------------------------------------------------------------------------
1 | true]) : null;
22 | }
23 |
24 | /**
25 | * Serialize a job.
26 | *
27 | * @return mixed|string|null
28 | */
29 | public function set($model, string $key, $value, array $attributes)
30 | {
31 | if (blank($value)) {
32 | return null;
33 | }
34 |
35 | if (! $value instanceof MiddlewareCollection) {
36 | throw new InvalidArgumentException(sprintf('Value provided must be an instance of %s.', MiddlewareCollection::class));
37 | }
38 |
39 | return SerializationHelper::serialize($value);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Casts/SerializeClosure.php:
--------------------------------------------------------------------------------
1 | true])->getClosure() : null;
24 | }
25 |
26 | /**
27 | * Serialize a closure.
28 | *
29 | * @return mixed|string|null
30 | *
31 | * @throws \Laravel\SerializableClosure\Exceptions\PhpVersionNotSupportedException
32 | */
33 | public function set($model, string $key, $value, array $attributes): ?string
34 | {
35 | if (blank($value)) {
36 | return null;
37 | }
38 |
39 | if ($value instanceof Closure === false && is_callable($value) === false) {
40 | throw new InvalidArgumentException('Value provided must be a closure or an invokable class.');
41 | }
42 |
43 | $closure = ClosureHelper::fromCallable($value);
44 |
45 | return SerializationHelper::serialize(new SerializableClosure($closure));
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Casts/Serialized.php:
--------------------------------------------------------------------------------
1 | true]) : null;
20 | }
21 |
22 | /**
23 | * Serialize a job.
24 | *
25 | * @return mixed|string|null
26 | */
27 | public function set($model, string $key, $value, array $attributes)
28 | {
29 | if (blank($value)) {
30 | return null;
31 | }
32 |
33 | return SerializationHelper::serialize($value);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Casts/SerializedModel.php:
--------------------------------------------------------------------------------
1 | true])->model : null;
23 | }
24 |
25 | /**
26 | * Serialize a model.
27 | *
28 | * @return mixed|string|null
29 | */
30 | public function set($model, string $key, $value, array $attributes)
31 | {
32 | if (blank($value)) {
33 | return null;
34 | }
35 |
36 | if (! $value instanceof Model) {
37 | throw new InvalidArgumentException('The provided value must be a model.');
38 | }
39 |
40 | return SerializationHelper::serialize(new SerializedModelData($value));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/ChunkableHaystackJob.php:
--------------------------------------------------------------------------------
1 | haystack)) {
30 | parent::dispatchNextChunk($job);
31 |
32 | return;
33 | }
34 |
35 | $this->prependToHaystack($job, $this->chunkInterval);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Concerns/ManagesBales.php:
--------------------------------------------------------------------------------
1 | bales()->first();
39 | }
40 |
41 | /**
42 | * Get the next job from the Haystack.
43 | */
44 | public function getNextJob(): ?NextJob
45 | {
46 | $jobRow = $this->getNextJobRow();
47 |
48 | if (! $jobRow instanceof HaystackBale) {
49 | return null;
50 | }
51 |
52 | // We'll retrieve the configured job which will have
53 | // the delay, queue and connection all set up.
54 |
55 | $job = $jobRow->configuredJob();
56 |
57 | // We'll now set the Haystack model on the job.
58 |
59 | $job->setHaystack($this)
60 | ->setHaystackBaleId($jobRow->getKey())
61 | ->setHaystackBaleAttempts($jobRow->attempts)
62 | ->setHaystackBaleRetryUntil($jobRow->retry_until);
63 |
64 | // We'll now apply any global middleware if it was provided to us
65 | // while building the Haystack.
66 |
67 | if ($this->middleware instanceof MiddlewareCollection) {
68 | $job->middleware = array_merge($job->middleware, $this->middleware->toMiddlewareArray());
69 | }
70 |
71 | // Apply default middleware. We'll need to make sure that
72 | // the job middleware is added to the top of the array.
73 |
74 | $defaultMiddleware = [
75 | new CheckFinished,
76 | new CheckAttempts,
77 | new IncrementAttempts,
78 | ];
79 |
80 | $job->middleware = array_merge($defaultMiddleware, $job->middleware);
81 |
82 | // Return the NextJob DTO which contains the job and the row!
83 |
84 | return new NextJob($job, $jobRow);
85 | }
86 |
87 | /**
88 | * Dispatch the next job.
89 | *
90 | *
91 | * @throws PhpVersionNotSupportedException
92 | */
93 | public function dispatchNextJob(?StackableJob $currentJob = null, int|CarbonInterface|null $delayInSecondsOrCarbon = null): void
94 | {
95 | // If the resume_at has been set, and the date is in the future, we're not allowed to process
96 | // the next job, so we stop.
97 |
98 | if ($this->resume_at instanceof CarbonInterface && $this->resume_at->isFuture()) {
99 | return;
100 | }
101 |
102 | if (is_null($currentJob) && $this->started === false) {
103 | $this->start();
104 |
105 | return;
106 | }
107 |
108 | // If the job has been provided, we will delete the haystack bale to prevent
109 | // the same bale being retrieved on the next job.
110 |
111 | if (isset($currentJob)) {
112 | HaystackBale::query()->whereKey($currentJob->getHaystackBaleId())->delete();
113 | }
114 |
115 | // If the delay in seconds has been provided, we need to pause the haystack by the
116 | // delay.
117 |
118 | if (isset($delayInSecondsOrCarbon)) {
119 | $this->pause(CarbonHelper::createFromSecondsOrCarbon($delayInSecondsOrCarbon));
120 |
121 | return;
122 | }
123 |
124 | // Now we'll query the next bale.
125 |
126 | $nextJob = $this->getNextJob();
127 |
128 | // If no next job was found, we'll stop.
129 |
130 | if (! $nextJob instanceof NextJob) {
131 | $this->finish();
132 |
133 | return;
134 | }
135 |
136 | dispatch($nextJob->job);
137 | }
138 |
139 | /**
140 | * Start the Haystack.
141 | *
142 | *
143 | * @throws PhpVersionNotSupportedException
144 | */
145 | public function start(): void
146 | {
147 | $this->update(['started_at' => now()]);
148 |
149 | $this->dispatchNextJob();
150 | }
151 |
152 | /**
153 | * Restart the haystack
154 | *
155 | *
156 | * @throws PhpVersionNotSupportedException
157 | */
158 | public function restart(): void
159 | {
160 | $this->dispatchNextJob();
161 | }
162 |
163 | /**
164 | * Cancel the haystack.
165 | *
166 | *
167 | * @throws PhpVersionNotSupportedException
168 | */
169 | public function cancel(): void
170 | {
171 | $this->finish(FinishStatus::Cancelled);
172 | }
173 |
174 | /**
175 | * Finish the Haystack.
176 | *
177 | *
178 | * @throws PhpVersionNotSupportedException
179 | */
180 | public function finish(FinishStatus $status = FinishStatus::Success): void
181 | {
182 | if ($this->finished === true) {
183 | return;
184 | }
185 |
186 | $this->update(['finished_at' => now()]);
187 |
188 | $callbacks = $this->getCallbacks();
189 |
190 | $data = $callbacks->isNotEmpty() ? $this->conditionallyGetAllData() : null;
191 |
192 | match ($status) {
193 | FinishStatus::Success => $this->invokeCallbacks($callbacks->onThen, $data),
194 | FinishStatus::Failure => $this->invokeCallbacks($callbacks->onCatch, $data),
195 | default => null,
196 | };
197 |
198 | // Always execute the finally closure.
199 |
200 | $this->invokeCallbacks($callbacks->onFinally, $data);
201 |
202 | // Now finally delete itself.
203 |
204 | if ($status === FinishStatus::Success && config('haystack.delete_finished_haystacks') === true) {
205 | $this->delete();
206 | }
207 | }
208 |
209 | /**
210 | * Fail the Haystack.
211 | *
212 | *
213 | * @throws PhpVersionNotSupportedException
214 | */
215 | public function fail(): void
216 | {
217 | $this->finish(FinishStatus::Failure);
218 | }
219 |
220 | /**
221 | * Add new jobs to the haystack.
222 | */
223 | public function addJobs(StackableJob|Collection|array $jobs, int $delayInSeconds = 0, ?string $queue = null, ?string $connection = null, bool $prepend = false): void
224 | {
225 | if ($jobs instanceof StackableJob) {
226 | $jobs = [$jobs];
227 | }
228 |
229 | if ($jobs instanceof Collection) {
230 | $jobs = $jobs->all();
231 | }
232 |
233 | $pendingJobs = [];
234 |
235 | foreach ($jobs as $job) {
236 | $pendingJobs[] = new PendingHaystackBale($job, $delayInSeconds, $queue, $connection, $prepend);
237 | }
238 |
239 | $this->addPendingJobs($pendingJobs);
240 | }
241 |
242 | /**
243 | * Add pending jobs to the haystack.
244 | */
245 | public function addPendingJobs(array $pendingJobs): void
246 | {
247 | $pendingJobRows = collect($pendingJobs)
248 | ->filter(fn ($pendingJob) => $pendingJob instanceof PendingHaystackBale)
249 | ->map(fn (PendingHaystackBale $pendingJob) => $pendingJob->toDatabaseRow($this))
250 | ->all();
251 |
252 | if (empty($pendingJobRows)) {
253 | return;
254 | }
255 |
256 | $this->bales()->insert($pendingJobRows);
257 | }
258 |
259 | /**
260 | * Execute the closures.
261 | *
262 | * @param array $closures
263 | *
264 | * @throws PhpVersionNotSupportedException
265 | */
266 | protected function invokeCallbacks(?array $closures, ?Collection $data = null): void
267 | {
268 | collect($closures)->each(function (SerializableClosure $closure) use ($data) {
269 | $closure($data);
270 | });
271 | }
272 |
273 | /**
274 | * Pause the haystack.
275 | *
276 | *
277 | * @throws PhpVersionNotSupportedException
278 | */
279 | public function pause(CarbonImmutable $resumeAt): void
280 | {
281 | $this->update(['resume_at' => $resumeAt]);
282 |
283 | $callbacks = $this->getCallbacks();
284 |
285 | if (empty($callbacks->onPaused)) {
286 | return;
287 | }
288 |
289 | $data = $this->conditionallyGetAllData();
290 |
291 | $this->invokeCallbacks($callbacks->onPaused, $data);
292 | }
293 |
294 | /**
295 | * Store data on the Haystack.
296 | *
297 | * @return ManagesBales|\Sammyjo20\LaravelHaystack\Models\Haystack
298 | */
299 | public function setData(string $key, mixed $value, ?string $cast = null): self
300 | {
301 | DataValidator::validateCast($value, $cast);
302 |
303 | $this->data()->updateOrCreate(['key' => $key], [
304 | 'cast' => $cast,
305 | 'value' => $value,
306 | ]);
307 |
308 | return $this;
309 | }
310 |
311 | /**
312 | * Retrieve data by a key from the Haystack.
313 | */
314 | public function getData(string $key, mixed $default = null): mixed
315 | {
316 | $data = $this->data()->where('key', $key)->first();
317 |
318 | return $data instanceof HaystackData ? $data->value : $default;
319 | }
320 |
321 | /**
322 | * Retrieve a shared model
323 | */
324 | public function getModel(string $key, mixed $default = null): mixed
325 | {
326 | return $this->getData('model:'.$key, $default);
327 | }
328 |
329 | /**
330 | * Set a model on a Haystack
331 | *
332 | * @return $this
333 | *
334 | * @throws HaystackModelExists
335 | */
336 | public function setModel(Model $model, ?string $key = null): static
337 | {
338 | $key = DataHelper::getModelKey($model, $key);
339 |
340 | if ($this->data()->where('key', $key)->exists()) {
341 | throw new HaystackModelExists($key);
342 | }
343 |
344 | return $this->setData($key, $model, SerializedModel::class);
345 | }
346 |
347 | /**
348 | * Retrieve all the data from the Haystack.
349 | */
350 | public function allData(bool $includeModels = false): Collection
351 | {
352 | $data = $this->data()
353 | ->when($includeModels === false, fn ($query) => $query->where('key', 'NOT LIKE', 'model:%'))
354 | ->orderBy('id')->get();
355 |
356 | return $data->mapWithKeys(function ($value, $key) {
357 | return [$value->key => $value->value];
358 | });
359 | }
360 |
361 | /**
362 | * Conditionally retrieve all the data from the Haystack depending on
363 | * if we are able to return the data.
364 | */
365 | protected function conditionallyGetAllData(): ?Collection
366 | {
367 | $returnAllData = config('haystack.return_all_haystack_data_when_finished', false);
368 |
369 | return $this->options->returnDataOnFinish === true && $returnAllData === true ? $this->allData() : null;
370 | }
371 |
372 | /**
373 | * Increment the bale attempts.
374 | */
375 | public function incrementBaleAttempts(StackableJob $job): void
376 | {
377 | HaystackBale::query()->whereKey($job->getHaystackBaleId())->increment('attempts');
378 | }
379 |
380 | /**
381 | * Set the retry-until on the job.
382 | */
383 | public function setBaleRetryUntil(StackableJob $job, int $retryUntil): void
384 | {
385 | HaystackBale::query()->whereKey($job->getHaystackBaleId())->update(['retry_until' => $retryUntil]);
386 | }
387 |
388 | /**
389 | * Get the callbacks on the Haystack.
390 | */
391 | public function getCallbacks(): CallbackCollection
392 | {
393 | return $this->callbacks ?? new CallbackCollection;
394 | }
395 | }
396 |
--------------------------------------------------------------------------------
/src/Concerns/Stackable.php:
--------------------------------------------------------------------------------
1 | haystack;
46 | }
47 |
48 | /**
49 | * Set the Haystack onto the job.
50 | *
51 | * @return $this
52 | */
53 | public function setHaystack(Haystack $haystack): static
54 | {
55 | $this->haystack = $haystack;
56 |
57 | return $this;
58 | }
59 |
60 | /**
61 | * Dispatch the next job in the Haystack.
62 | *
63 | * @return $this
64 | *
65 | * @throws StackableException
66 | */
67 | public function nextJob(int|CarbonInterface|null $delayInSecondsOrCarbon = null): static
68 | {
69 | if (config('haystack.process_automatically', false) === true) {
70 | throw new StackableException('The "nextJob" method is unavailable when "haystack.process_automatically" is enabled.');
71 | }
72 |
73 | $this->haystack->dispatchNextJob($this, $delayInSecondsOrCarbon);
74 |
75 | return $this;
76 | }
77 |
78 | /**
79 | * Dispatch the next bale in the haystack. Yee-haw!
80 | *
81 | * @return $this
82 | *
83 | * @throws StackableException
84 | */
85 | public function nextBale(int|CarbonInterface|null $delayInSecondsOrCarbon = null): static
86 | {
87 | return $this->nextJob($delayInSecondsOrCarbon);
88 | }
89 |
90 | /**
91 | * Release the job for haystack to process later.
92 | *
93 | * @return $this
94 | */
95 | public function longRelease(int|CarbonInterface $delayInSecondsOrCarbon): static
96 | {
97 | $resumeAt = CarbonHelper::createFromSecondsOrCarbon($delayInSecondsOrCarbon);
98 |
99 | $this->haystack->pause($resumeAt);
100 |
101 | return $this;
102 | }
103 |
104 | /**
105 | * Finish the Haystack.
106 | *
107 | * @return $this
108 | */
109 | public function finishHaystack(): static
110 | {
111 | $this->haystack->finish();
112 |
113 | return $this;
114 | }
115 |
116 | /**
117 | * Fail the job stack.
118 | *
119 | * @return $this
120 | */
121 | public function failHaystack(): static
122 | {
123 | $this->haystack->finish(FinishStatus::Failure);
124 |
125 | return $this;
126 | }
127 |
128 | /**
129 | * Append jobs to the haystack.
130 | *
131 | * @return $this
132 | */
133 | public function appendToHaystack(StackableJob|Collection|array $jobs, int $delayInSeconds = 0, ?string $queue = null, ?string $connection = null): static
134 | {
135 | $this->haystack->addJobs($jobs, $delayInSeconds, $queue, $connection, false);
136 |
137 | return $this;
138 | }
139 |
140 | /**
141 | * Prepend jobs to the haystack.
142 | *
143 | * @return $this
144 | */
145 | public function prependToHaystack(StackableJob|Collection|array $jobs, int $delayInSeconds = 0, ?string $queue = null, ?string $connection = null): static
146 | {
147 | $this->haystack->addJobs($jobs, $delayInSeconds, $queue, $connection, true);
148 |
149 | return $this;
150 | }
151 |
152 | /**
153 | * Get the haystack bale id
154 | */
155 | public function getHaystackBaleId(): int
156 | {
157 | return $this->haystackBaleId;
158 | }
159 |
160 | /**
161 | * Set the Haystack bale ID.
162 | *
163 | * @return $this
164 | */
165 | public function setHaystackBaleId(int $haystackBaleId): static
166 | {
167 | $this->haystackBaleId = $haystackBaleId;
168 |
169 | return $this;
170 | }
171 |
172 | /**
173 | * Pause the haystack. We also need to delete the current row.
174 | *
175 | * @return $this
176 | *
177 | * @throws StackableException
178 | */
179 | public function pauseHaystack(int|CarbonInterface $delayInSecondsOrCarbon): static
180 | {
181 | if (config('haystack.process_automatically', false) === false) {
182 | throw new StackableException('The "pauseHaystack" method is unavailable when "haystack.process_automatically" is disabled. Use the "nextJob" with a delay provided instead.');
183 | }
184 |
185 | $resumeAt = CarbonHelper::createFromSecondsOrCarbon($delayInSecondsOrCarbon);
186 |
187 | $this->haystack->pause($resumeAt);
188 |
189 | // We need to make sure that we delete the current haystack bale to stop it
190 | // from being processed when the haystack is resumed.
191 |
192 | HaystackBale::query()->whereKey($this->getHaystackBaleId())->delete();
193 |
194 | return $this;
195 | }
196 |
197 | /**
198 | * Set data on the haystack.
199 | *
200 | * @return $this
201 | */
202 | public function setHaystackData(string $key, mixed $value, ?string $cast = null): static
203 | {
204 | $this->haystack->setData($key, $value, $cast);
205 |
206 | return $this;
207 | }
208 |
209 | /**
210 | * Get data on the haystack.
211 | */
212 | public function getHaystackData(string $key, mixed $default = null): mixed
213 | {
214 | return $this->haystack->getData($key, $default);
215 | }
216 |
217 | /**
218 | * Get a shared model
219 | */
220 | public function getHaystackModel(string $model, mixed $default = null): ?Model
221 | {
222 | return $this->haystack->getModel($model, $default);
223 | }
224 |
225 | /**
226 | * Set a shared model
227 | *
228 | * @return $this
229 | *
230 | * @throws \Sammyjo20\LaravelHaystack\Exceptions\HaystackModelExists
231 | */
232 | public function setHaystackModel(Model $model, string $key): static
233 | {
234 | $this->haystack->setModel($model, $key);
235 |
236 | return $this;
237 | }
238 |
239 | /**
240 | * Get all data on the haystack.
241 | *
242 | * @return mixed
243 | */
244 | public function allHaystackData(): Collection
245 | {
246 | return $this->haystack->allData();
247 | }
248 |
249 | /**
250 | * Get the haystack bale attempts.
251 | */
252 | public function getHaystackBaleAttempts(): int
253 | {
254 | return $this->haystackBaleAttempts;
255 | }
256 |
257 | /**
258 | * Set the haystack bale attempts.
259 | *
260 | * @return $this
261 | */
262 | public function setHaystackBaleAttempts(int $attempts): static
263 | {
264 | $this->haystackBaleAttempts = $attempts;
265 |
266 | return $this;
267 | }
268 |
269 | /**
270 | * Get the haystack bale retry-until.
271 | */
272 | public function getHaystackBaleRetryUntil(): ?int
273 | {
274 | return $this->haystackBaleRetryUntil;
275 | }
276 |
277 | /**
278 | * Set the haystack bale retry-until.
279 | *
280 | * @return $this
281 | */
282 | public function setHaystackBaleRetryUntil(?int $retryUntil): static
283 | {
284 | $this->haystackBaleRetryUntil = $retryUntil;
285 |
286 | return $this;
287 | }
288 |
289 | /**
290 | * Get the options on the Haystack
291 | */
292 | public function getHaystackOptions(): HaystackOptions
293 | {
294 | return $this->haystack->options;
295 | }
296 |
297 | /**
298 | * Retrieve a haystack option
299 | */
300 | public function getHaystackOption(string $option, mixed $default = null): mixed
301 | {
302 | return $this->haystack->options->$option ?? $default;
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/src/Console/Commands/HaystackInstall.php:
--------------------------------------------------------------------------------
1 | info(' 🚀 | Installing Haystack');
27 |
28 | $this->info(' 🪐 | Publishing migrations...');
29 |
30 | $this->callSilently('vendor:publish', ['--tag' => 'haystack-migrations']);
31 |
32 | $this->info(' 🔭 | Publishing config...');
33 |
34 | $this->callSilently('vendor:publish', ['--tag' => 'haystack-config']);
35 |
36 | $runMigrations = $this->confirm('Would you like to run migrations?', false);
37 |
38 | if ($runMigrations) {
39 | $this->callSilently('migrate');
40 |
41 | $this->info(' 🎯 | Migrations run successfully');
42 | }
43 |
44 | if ($this->confirm(' 🤩 | Would you like to star the repo on GitHub?')) {
45 | $repoUrl = 'https://github.com/sammyjo20/laravel-haystack';
46 |
47 | if (PHP_OS_FAMILY == 'Darwin') {
48 | exec("open {$repoUrl}");
49 | }
50 |
51 | if (PHP_OS_FAMILY == 'Windows') {
52 | exec("start {$repoUrl}");
53 | }
54 |
55 | if (PHP_OS_FAMILY == 'Linux') {
56 | exec("xdg-open {$repoUrl}");
57 | }
58 | }
59 |
60 | $this->info(' ✅ | Haystack has been installed. Thank you for using Haystack. Happy developing! ❤️');
61 |
62 | return self::SUCCESS;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Console/Commands/HaystacksClear.php:
--------------------------------------------------------------------------------
1 | delete();
19 |
20 | $this->info("Cleared $count haystacks");
21 |
22 | return self::SUCCESS;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Console/Commands/HaystacksForget.php:
--------------------------------------------------------------------------------
1 | argument('id'));
19 |
20 | if (! $haystack) {
21 | $this->error('No haystack matches the given ID.');
22 |
23 | return self::FAILURE;
24 | }
25 |
26 | $haystack->delete();
27 | $this->info('Haystack deleted successfully!');
28 |
29 | return self::SUCCESS;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Console/Commands/HaystacksResume.php:
--------------------------------------------------------------------------------
1 | where('resume_at', '<=', now())->cursor();
19 |
20 | foreach ($haystacks as $haystack) {
21 | $haystack->update(['resume_at' => null]);
22 | $haystack->dispatchNextJob();
23 | }
24 |
25 | return self::SUCCESS;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Contracts/StackableJob.php:
--------------------------------------------------------------------------------
1 | addCallback('onThen', $closure);
43 | }
44 |
45 | /**
46 | * Add a "catch" callback
47 | *
48 | * @return $this
49 | *
50 | * @throws \Laravel\SerializableClosure\Exceptions\PhpVersionNotSupportedException
51 | */
52 | public function addCatch(Closure|callable $closure): static
53 | {
54 | return $this->addCallback('onCatch', $closure);
55 | }
56 |
57 | /**
58 | * Add a "finally" callback
59 | *
60 | * @return $this
61 | *
62 | * @throws \Laravel\SerializableClosure\Exceptions\PhpVersionNotSupportedException
63 | */
64 | public function addFinally(Closure|callable $closure): static
65 | {
66 | return $this->addCallback('onFinally', $closure);
67 | }
68 |
69 | /**
70 | * Add a "paused" callback
71 | *
72 | * @return $this
73 | *
74 | * @throws \Laravel\SerializableClosure\Exceptions\PhpVersionNotSupportedException
75 | */
76 | public function addPaused(Closure|callable $closure): static
77 | {
78 | return $this->addCallback('onPaused', $closure);
79 | }
80 |
81 | /**
82 | * Add a callback to a given property
83 | *
84 | * @return $this
85 | *
86 | * @throws \Laravel\SerializableClosure\Exceptions\PhpVersionNotSupportedException
87 | */
88 | protected function addCallback(string $property, Closure|callable $closure): static
89 | {
90 | $this->$property[] = new SerializableClosure(ClosureHelper::fromCallable($closure));
91 |
92 | return $this;
93 | }
94 |
95 | /**
96 | * Check if the callbacks are empty.
97 | */
98 | public function isEmpty(): bool
99 | {
100 | return empty($this->onThen) && empty($this->onCatch) && empty($this->onFinally) && empty($this->onPaused);
101 | }
102 |
103 | /**
104 | * Check if the callbacks are not empty
105 | */
106 | public function isNotEmpty(): bool
107 | {
108 | return ! $this->isEmpty();
109 | }
110 |
111 | /**
112 | * Convert the object ready to be serialized
113 | *
114 | * @return $this|null
115 | */
116 | public function toSerializable(): ?static
117 | {
118 | return $this->isNotEmpty() ? $this : null;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Data/HaystackOptions.php:
--------------------------------------------------------------------------------
1 | $name = $value;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Data/MiddlewareCollection.php:
--------------------------------------------------------------------------------
1 | $value;
30 | }
31 |
32 | $this->data[] = new SerializableClosure(ClosureHelper::fromCallable($value));
33 |
34 | return $this;
35 | }
36 |
37 | /**
38 | * Call the whole middleware stack and convert it into an array
39 | */
40 | public function toMiddlewareArray(): array
41 | {
42 | return collect($this->data)
43 | ->map(function (SerializableClosure $closure) {
44 | $result = $closure();
45 |
46 | return is_array($result) ? $result : [$result];
47 | })
48 | ->flatten()
49 | ->filter(fn ($value) => is_object($value))
50 | ->toArray();
51 | }
52 |
53 | /**
54 | * Check if the middleware is empty
55 | */
56 | public function isEmpty(): bool
57 | {
58 | return empty($this->data);
59 | }
60 |
61 | /**
62 | * Check if the middleware is not empty
63 | */
64 | public function isNotEmpty(): bool
65 | {
66 | return ! $this->isEmpty();
67 | }
68 |
69 | /**
70 | * Convert the object ready to be serialized
71 | *
72 | * @return $this|null
73 | */
74 | public function toSerializable(): ?static
75 | {
76 | return $this->isNotEmpty() ? $this : null;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Data/NextJob.php:
--------------------------------------------------------------------------------
1 | job->delay;
24 | $nativeQueue = $this->job->queue;
25 | $nativeConnection = $this->job->connection;
26 |
27 | if (isset($nativeDelay) && $this->delayInSeconds <= 0) {
28 | $this->delayInSeconds = $nativeDelay;
29 | }
30 |
31 | if (isset($nativeQueue) && ! isset($this->queue)) {
32 | $this->queue = $nativeQueue;
33 | }
34 |
35 | if (isset($nativeConnection) && ! isset($this->connection)) {
36 | $this->connection = $nativeConnection;
37 | }
38 | }
39 |
40 | /**
41 | * Convert to a haystack bale for casting.
42 | */
43 | public function toDatabaseRow(Haystack $haystack): array
44 | {
45 | $now = Carbon::now();
46 |
47 | return $haystack->bales()->make([
48 | 'created_at' => $now,
49 | 'updated_at' => $now,
50 | 'job' => $this->job,
51 | 'delay' => $this->delayInSeconds,
52 | 'on_queue' => $this->queue,
53 | 'on_connection' => $this->connection,
54 | 'priority' => $this->priority,
55 | ])->getAttributes();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Data/SerializedModel.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(
26 | __DIR__.'/../config/haystack.php',
27 | 'haystack'
28 | );
29 | }
30 |
31 | /**
32 | * Bootstrap any package services.
33 | */
34 | public function boot(): void
35 | {
36 | $this->publishConfigAndMigrations()
37 | ->registerCommands()
38 | ->registerQueueListeners();
39 | }
40 |
41 | /**
42 | * Public the config file and migrations
43 | *
44 | * @return $this
45 | */
46 | protected function publishConfigAndMigrations(): static
47 | {
48 | $this->publishes([
49 | __DIR__.'/../config/haystack.php' => config_path('haystack.php'),
50 | ], 'haystack-config');
51 |
52 | $this->publishes([
53 | __DIR__.'/../database/migrations/create_haystacks_table.php.stub' => database_path('migrations/'.now()->format('Y_m_d_His').'_create_haystacks_table.php'),
54 | __DIR__.'/../database/migrations/create_haystack_bales_table.php.stub' => database_path('migrations/'.now()->addSeconds(1)->format('Y_m_d_His').'_create_haystack_bales_table.php'),
55 | __DIR__.'/../database/migrations/create_haystack_data_table.php.stub' => database_path('migrations/'.now()->addSeconds(2)->format('Y_m_d_His').'_create_haystack_data_table.php'),
56 | ], 'haystack-migrations');
57 |
58 | return $this;
59 | }
60 |
61 | /**
62 | * Register commands
63 | *
64 | * @return $this
65 | */
66 | protected function registerCommands(): static
67 | {
68 | if (! $this->app->runningInConsole()) {
69 | return $this;
70 | }
71 |
72 | $this->commands([
73 | HaystacksClear::class,
74 | HaystacksForget::class,
75 | HaystacksResume::class,
76 | HaystackInstall::class,
77 | ]);
78 |
79 | return $this;
80 | }
81 |
82 | /**
83 | * Listen to the queue events.
84 | *
85 | * @return $this
86 | */
87 | public function registerQueueListeners(): static
88 | {
89 | if (config('haystack.process_automatically') !== true) {
90 | return $this;
91 | }
92 |
93 | Queue::createPayloadUsing(fn ($connection, $queue, $payload) => JobEventListener::make()->createPayloadUsing($connection, $queue, $payload));
94 |
95 | Queue::after(fn (JobProcessed $event) => JobEventListener::make()->handleJobProcessed($event));
96 |
97 | Queue::failing(fn (JobFailed $event) => JobEventListener::make()->handleFailedJob($event));
98 |
99 | return $this;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/Helpers/CarbonHelper.php:
--------------------------------------------------------------------------------
1 | addSeconds($value) : $value->toImmutable();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Helpers/ClosureHelper.php:
--------------------------------------------------------------------------------
1 | $value();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Helpers/DataHelper.php:
--------------------------------------------------------------------------------
1 | $data];
37 | }
38 |
39 | // If we are about to process a StackableJob, we should add the "haystack_id"
40 | // to the job payload. This will help us resolve the haystack when the job
41 | // is processed by the "handleJobProcessed" event.
42 |
43 | $data = array_merge($payload['data'], [
44 | 'haystack_id' => $command->getHaystack()->getKey(),
45 | ]);
46 |
47 | return ['data' => $data];
48 | }
49 |
50 | /**
51 | * Handle the "JobProcessed" event.
52 | */
53 | public function handleJobProcessed(JobProcessed $event): void
54 | {
55 | $processedJob = $event->job;
56 | $payload = $processedJob->payload();
57 |
58 | // We'll firstly attempt to get the haystack from the payload. The reason
59 | // we do this first, is because if we attempt to unserialize the job
60 | // first and the haystack is deleted, it will throw an exception
61 | // because the model now longer exists.
62 |
63 | $haystack = $this->getHaystackFromPayload($payload);
64 |
65 | if (! $haystack instanceof Haystack) {
66 | return;
67 | }
68 |
69 | // We will next attempt to decode the "command" from the job payload.
70 | // The command data contains a serialized version of the job.
71 |
72 | $job = $this->unserializeJobFromPayload($payload);
73 |
74 | if (! $job instanceof StackableJob) {
75 | return;
76 | }
77 |
78 | // Once we have unserialized the job, and we know it is a stackable job
79 | // we should check if the job has been released. If the job has been
80 | // released we will wait for the job to be processed again. If the
81 | // processed job is a "SyncJob" we should ignore the check since
82 | // sync jobs will be processed straight away.
83 |
84 | if ($processedJob instanceof SyncJob === false && $processedJob->isReleased() === true) {
85 | return;
86 | }
87 |
88 | // Once we have found the Haystack, we'll check if the current job has
89 | // failed. If it has, then we'll just stop here. If it has failed
90 | // the fail handler will continue for us.
91 |
92 | if ($processedJob->hasFailed()) {
93 | return;
94 | }
95 |
96 | // Dispatch the next job...
97 |
98 | $haystack->dispatchNextJob($job);
99 | }
100 |
101 | /**
102 | * Handle the "JobFailed" event.
103 | */
104 | public function handleFailedJob(JobFailed $event): void
105 | {
106 | $processedJob = $event->job;
107 | $payload = $processedJob->payload();
108 |
109 | // We'll firstly attempt to get the haystack from the payload. The reason
110 | // we do this first, is because if we attempt to unserialize the job
111 | // first and the haystack is deleted, it will throw an exception
112 | // because the model now longer exists.
113 |
114 | $haystack = $this->getHaystackFromPayload($payload);
115 |
116 | if (! $haystack instanceof Haystack) {
117 | return;
118 | }
119 |
120 | // We will next attempt to decode the "command" from the job payload.
121 | // The command data contains a serialized version of the job.
122 |
123 | $job = $this->unserializeJobFromPayload($payload);
124 |
125 | if (! $job instanceof StackableJob) {
126 | return;
127 | }
128 |
129 | // If allow failures is turned on, we'll dispatch the next job.
130 |
131 | if ($haystack->options->allowFailures === true) {
132 | $haystack->dispatchNextJob($job);
133 |
134 | return;
135 | }
136 |
137 | // Otherwise we'll fail the Haystack
138 |
139 | $haystack->fail();
140 | }
141 |
142 | /**
143 | * Unserialize the job from the job payload.
144 | */
145 | private function unserializeJobFromPayload(array $payload): ?object
146 | {
147 | if (! isset($payload['data']['command'])) {
148 | return null;
149 | }
150 |
151 | return SerializationHelper::unserialize($payload['data']['command'], ['allowed_classes' => true]);
152 | }
153 |
154 | /**
155 | * Attempt to find the haystack model from the job payload.
156 | */
157 | private function getHaystackFromPayload(array $payload): ?Haystack
158 | {
159 | $haystackId = $payload['data']['haystack_id'] ?? null;
160 |
161 | if (blank($haystackId)) {
162 | return null;
163 | }
164 |
165 | return Haystack::find($haystackId);
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/Middleware/CheckAttempts.php:
--------------------------------------------------------------------------------
1 | getHaystackBaleRetryUntil())) {
25 | $exceededRetryUntil = now()->greaterThan(CarbonImmutable::parse($job->getHaystackBaleRetryUntil()));
26 | } else {
27 | $maxTries = $job->tries ?? 1;
28 | }
29 |
30 | $exceededLimit = (isset($maxTries) && $job->getHaystackBaleAttempts() >= $maxTries) || $exceededRetryUntil === true;
31 |
32 | if ($exceededLimit === true) {
33 | $exception = ExceptionHelper::maxAttemptsExceededException($job);
34 |
35 | $job->fail($exception);
36 |
37 | throw $exception;
38 | }
39 |
40 | $next($job);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Middleware/CheckFinished.php:
--------------------------------------------------------------------------------
1 | getHaystack()->finished === true) {
17 | return;
18 | }
19 |
20 | $next($job);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Middleware/IncrementAttempts.php:
--------------------------------------------------------------------------------
1 | getHaystack();
17 |
18 | // First we'll increment the bale attempts.
19 |
20 | $haystack->incrementBaleAttempts($job);
21 |
22 | // When the "retryUntil" method is used we need to store the timestamp
23 | // that was generated by the queue worker. if the job is paused and
24 | // retried later we will resolve this timestamp and then check if
25 | // it has expired from the original queue push.
26 |
27 | if (method_exists($job, 'retryUntil') && is_null($job->getHaystackBaleRetryUntil())) {
28 | $retryUntil = $job->job?->retryUntil();
29 |
30 | if (is_int($retryUntil)) {
31 | $haystack->setBaleRetryUntil($job, $retryUntil);
32 | }
33 | }
34 |
35 | $next($job);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Models/Haystack.php:
--------------------------------------------------------------------------------
1 | CallbackCollectionCast::class,
46 | 'middleware' => MiddlewareCollectionCast::class,
47 | 'options' => Serialized::class,
48 | 'started_at' => 'immutable_datetime',
49 | 'resume_at' => 'immutable_datetime',
50 | 'finished_at' => 'immutable_datetime',
51 | ];
52 |
53 | /**
54 | * Create a new factory instance for the model.
55 | *
56 | * @return \Illuminate\Database\Eloquent\Factories\Factory
57 | */
58 | protected static function newFactory()
59 | {
60 | return HaystackFactory::new();
61 | }
62 |
63 | /**
64 | * Get the prunable model query.
65 | */
66 | public function prunable(): Builder
67 | {
68 | $staleHaystackDays = config('haystack.keep_stale_haystacks_for_days', 0);
69 | $finishedHaystackDays = config('haystack.keep_finished_haystacks_for_days', 0);
70 |
71 | return static::query()
72 | ->where(function ($query) use ($staleHaystackDays) {
73 | $query->whereNull('finished_at')->where('started_at', '<=', now()->subDays($staleHaystackDays));
74 | })
75 | ->orWhere(function ($query) use ($finishedHaystackDays) {
76 | $query->whereNotNull('started_at')->where('finished_at', '<=', now()->subDays($finishedHaystackDays));
77 | });
78 | }
79 |
80 | /**
81 | * The Haystack's bales.
82 | */
83 | public function bales(): HasMany
84 | {
85 | return $this->hasMany(HaystackBale::class, 'haystack_id', 'id')->orderBy('priority', 'desc')->orderBy('id', 'asc');
86 | }
87 |
88 | /**
89 | * The Haystack's data.
90 | */
91 | public function data(): HasMany
92 | {
93 | return $this->hasMany(HaystackData::class);
94 | }
95 |
96 | /**
97 | * Start building a Haystack.
98 | */
99 | public static function build(): HaystackBuilder
100 | {
101 | return new HaystackBuilder;
102 | }
103 |
104 | /**
105 | * Denotes if the haystack has started.
106 | */
107 | public function getStartedAttribute(): bool
108 | {
109 | return $this->started_at instanceof CarbonImmutable;
110 | }
111 |
112 | /**
113 | * Denotes if the haystack has finished.
114 | */
115 | public function getFinishedAttribute(): bool
116 | {
117 | return $this->finished_at instanceof CarbonImmutable;
118 | }
119 |
120 | /**
121 | * Get the current connection name for the model.
122 | */
123 | public function getConnectionName(): string
124 | {
125 | return config('haystack.db_connection');
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Models/HaystackBale.php:
--------------------------------------------------------------------------------
1 | Serialized::class,
28 | 'priority' => 'boolean',
29 | ];
30 |
31 | /**
32 | * Create a new factory instance for the model.
33 | *
34 | * @return \Illuminate\Database\Eloquent\Factories\Factory
35 | */
36 | protected static function newFactory()
37 | {
38 | return HaystackBaleFactory::new();
39 | }
40 |
41 | /**
42 | * The Haystack this row belongs to.
43 | */
44 | public function haystack(): BelongsTo
45 | {
46 | return $this->belongsTo(Haystack::class);
47 | }
48 |
49 | /**
50 | * Get the job already configured.
51 | */
52 | public function configuredJob(): StackableJob
53 | {
54 | $job = $this->job;
55 |
56 | if ($this->delay > 0) {
57 | $job->delay($this->delay);
58 | }
59 |
60 | if (filled($this->on_queue)) {
61 | $job->onQueue($this->on_queue);
62 | }
63 |
64 | if (filled($this->on_connection)) {
65 | $job->onConnection($this->on_connection);
66 | }
67 |
68 | return $job;
69 | }
70 |
71 | /**
72 | * Get the current connection name for the model.
73 | */
74 | public function getConnectionName(): string
75 | {
76 | return config('haystack.db_connection');
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Models/HaystackData.php:
--------------------------------------------------------------------------------
1 | belongsTo(Haystack::class);
37 | }
38 |
39 | /**
40 | * Set the cast attribute and apply the casts.
41 | */
42 | public function setCastAttribute(?string $cast): void
43 | {
44 | if (! is_null($cast)) {
45 | $this->casts = ['value' => $cast];
46 | }
47 |
48 | $this->attributes['cast'] = $cast;
49 | $this->attributes['value'] = null;
50 | }
51 |
52 | /**
53 | * Get the cast value.
54 | */
55 | public function getValueAttribute($value): mixed
56 | {
57 | if (blank($this->cast)) {
58 | return $value;
59 | }
60 |
61 | // We'll now manually add the cast and attempt to cast the attribute.
62 |
63 | $this->casts = ['value' => $this->cast];
64 |
65 | return $this->castAttribute('value', $value);
66 | }
67 |
68 | /**
69 | * Get the current connection name for the model.
70 | */
71 | public function getConnectionName(): string
72 | {
73 | return config('haystack.db_connection');
74 | }
75 | }
76 |
--------------------------------------------------------------------------------