├── .php_cs.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── artisan-dispatchable.php └── src ├── ArtisanDispatchableServiceProvider.php ├── ArtisanJob.php ├── ArtisanJobRepository.php ├── Composer.php ├── Console ├── CacheArtisanDispatchableJobsCommand.php └── ClearArtisanDispatchableJobsCommand.php ├── DiscoverArtisanJobs.php ├── DiscoveredArtisanJob.php ├── Exceptions ├── ModelNotFound.php └── RequiredOptionMissing.php └── Jobs └── ArtisanDispatchable.php /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-artisan-dispatchable` will be documented in this file. 4 | 5 | ## 1.5.2 - 2025-02-21 6 | 7 | ### What's Changed 8 | 9 | * Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-artisan-dispatchable/pull/14 10 | 11 | **Full Changelog**: https://github.com/spatie/laravel-artisan-dispatchable/compare/1.5.1...1.5.2 12 | 13 | ## 1.5.1 - 2025-01-22 14 | 15 | ### What's Changed 16 | 17 | * Fix command descriptions by @inmula in https://github.com/spatie/laravel-artisan-dispatchable/pull/13 18 | 19 | ### New Contributors 20 | 21 | * @inmula made their first contribution in https://github.com/spatie/laravel-artisan-dispatchable/pull/13 22 | 23 | **Full Changelog**: https://github.com/spatie/laravel-artisan-dispatchable/compare/1.5.0...1.5.1 24 | 25 | ## 1.5.0 - 2024-03-08 26 | 27 | ### What's Changed 28 | 29 | * Laravel 11.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-artisan-dispatchable/pull/11 30 | 31 | **Full Changelog**: https://github.com/spatie/laravel-artisan-dispatchable/compare/1.4.0...1.5.0 32 | 33 | ## 1.4.0 - 2023-08-30 34 | 35 | ### What's Changed 36 | 37 | - Allow Optional Parameters by @audiojames in https://github.com/spatie/laravel-artisan-dispatchable/pull/10 38 | 39 | ### New Contributors 40 | 41 | - @audiojames made their first contribution in https://github.com/spatie/laravel-artisan-dispatchable/pull/10 42 | 43 | **Full Changelog**: https://github.com/spatie/laravel-artisan-dispatchable/compare/1.3.2...1.4.0 44 | 45 | ## 1.3.2 - 2023-02-20 46 | 47 | ### What's Changed 48 | 49 | - support spatie/phpunit-snapshot-assertions v5 by @rjocoleman in https://github.com/spatie/laravel-artisan-dispatchable/pull/9 50 | 51 | ### New Contributors 52 | 53 | - @rjocoleman made their first contribution in https://github.com/spatie/laravel-artisan-dispatchable/pull/9 54 | 55 | **Full Changelog**: https://github.com/spatie/laravel-artisan-dispatchable/compare/1.3.1...1.3.2 56 | 57 | ## 1.3.1 - 2023-01-24 58 | 59 | ### What's Changed 60 | 61 | - Refactor tests to Pest by @alexmanase in https://github.com/spatie/laravel-artisan-dispatchable/pull/7 62 | - Laravel 10.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-artisan-dispatchable/pull/8 63 | 64 | ### New Contributors 65 | 66 | - @alexmanase made their first contribution in https://github.com/spatie/laravel-artisan-dispatchable/pull/7 67 | - @laravel-shift made their first contribution in https://github.com/spatie/laravel-artisan-dispatchable/pull/8 68 | 69 | **Full Changelog**: https://github.com/spatie/laravel-artisan-dispatchable/compare/1.3.0...1.3.1 70 | 71 | ## 1.3.0 - 2022-01-19 72 | 73 | - support Laravel 9 74 | 75 | ## 1.2.0 - 2021-06-29 76 | 77 | - add prefix option 78 | 79 | ## 1.1.0 - 2021-06-22 80 | 81 | - add support for arguments without a type 82 | 83 | ## 1.0.0 - 2021-06-14 84 | 85 | - initial release 86 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) spatie 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 | # Dispatch Laravel jobs via Artisan 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-artisan-dispatchable.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-artisan-dispatchable) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-artisan-dispatchable.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-artisan-dispatchable) 5 | 6 | This package can register jobs as Artisan commands. All you need to do is let your job implement the empty `ArtisanDispatchable` interface. 7 | 8 | ```php 9 | use Illuminate\Contracts\Queue\ShouldQueue; 10 | use Spatie\ArtisanDispatchable\Jobs\ArtisanDispatchable; 11 | 12 | class ProcessPodcast implements ShouldQueue, ArtisanDispatchable 13 | { 14 | public function handle() 15 | { 16 | // perform some work... 17 | } 18 | } 19 | ``` 20 | 21 | This allows the job to be executed via Artisan. 22 | 23 | ```bash 24 | php artisan process-podcast 25 | ``` 26 | 27 | ## Why we created this package 28 | 29 | [Laravel's scheduler](https://laravel.com/docs/master/scheduling#introduction) will perform all tasks sequentially. When you add a scheduled task to the scheduler, the task should perform its work as fast as possible, so no other tasks will have to wait. 30 | 31 | If you have a task that needs to run every minute and its runtime is close to a minute, you should not use a simple Artisan command, as this will result in the delay of all other minute-ly tasks. 32 | 33 | Long-running tasks should be performed by jobs that perform their work on the queue. Laravel has [the ability to schedule queued jobs](https://laravel.com/docs/master/scheduling#scheduling-queued-jobs). This way, those tasks will not block the scheduler. 34 | 35 | ```php 36 | $schedule->job(new ProcessPodcast)->everyFiveMinutes(); 37 | ```` 38 | 39 | The downside of this approach is that you cannot run that job via Artisan anymore. You have to choose between using an artisan command + blocking the scheduler on the one hand, and job + not blocking the scheduler on the other hand. 40 | 41 | Using our package, you don't have to make that choice anymore. When letting your job implement `Spatie\ArtisanDispatchable\Jobs\ArtisanDispatchable`, you will not block the scheduler and can still execute the logic via Artisan. 42 | 43 | ## Support us 44 | 45 | [](https://spatie.be/github-ad-click/laravel-artisan-dispatchable) 46 | 47 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 48 | 49 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 50 | 51 | ## Installation 52 | 53 | You can install the package via composer: 54 | 55 | ```bash 56 | composer require spatie/laravel-artisan-dispatchable 57 | ``` 58 | 59 | Optionally, you can publish the config file with: 60 | 61 | ```bash 62 | php artisan vendor:publish --provider="Spatie\ArtisanDispatchable\ArtisanDispatchableServiceProvider" --tag="artisan-dispatchable-config" 63 | ``` 64 | 65 | This is the contents of the published config file: 66 | 67 | ```php 68 | return [ 69 | /* 70 | * These directories will be scanned for dispatchable jobs. They 71 | * will be registered automatically to Artisan. 72 | */ 73 | 'auto_discover_dispatchable_jobs' => [ 74 | app()->path(), 75 | ], 76 | 77 | /* 78 | * This directory will be used as the base path when scanning 79 | * for dispatchable jobs. 80 | */ 81 | 'auto_discover_base_path' => base_path(), 82 | 83 | /* 84 | * In production, you likely don't want the package to auto-discover dispatchable 85 | * jobs every time Artisan is invoked. The package can cache discovered job. 86 | * 87 | * Here you can specify where the cache should be stored. 88 | */ 89 | 'cache_file' => storage_path('app/artisan-dispatchable/artisan-dispatchable-jobs.php'), 90 | 91 | /** 92 | * Here you can specify the prefix to be used for all dispatchable jobs. 93 | */ 94 | 'command_name_prefix' => '', 95 | ]; 96 | ``` 97 | 98 | ## Usage 99 | 100 | All you need to do is let your job implement the empty `ArtisanDispatchable` interface. 101 | 102 | ```php 103 | use Illuminate\Contracts\Queue\ShouldQueue; 104 | use Spatie\ArtisanDispatchable\Jobs\ArtisanDispatchable; 105 | 106 | class ProcessPodcast implements ShouldQueue, ArtisanDispatchable 107 | { 108 | public function handle() 109 | { 110 | // perform some work... 111 | } 112 | } 113 | ``` 114 | 115 | This allows the job to be executed via Artisan. 116 | 117 | ```bash 118 | php artisan process-podcast 119 | ``` 120 | 121 | This job will not be queued, but will be immediately executed inside the executed artisan command. 122 | 123 | ### Queueing jobs via Artisan 124 | 125 | If you want to put your job on the queue instead of executing it immediately, add the `queued` option. 126 | 127 | ```bash 128 | php artisan process-podcast --queued 129 | ``` 130 | 131 | ### Passing arguments to a job 132 | 133 | If your job has constructor arguments, you may pass those arguments via options on the artisan command. 134 | 135 | ```php 136 | use Illuminate\Contracts\Queue\ShouldQueue; 137 | use Spatie\ArtisanDispatchable\Jobs\ArtisanDispatchable; 138 | 139 | 140 | class ProcessPodcast implements ShouldQueue, ArtisanDispatchable 141 | { 142 | public function __construct( 143 | string $myFirstArgument, 144 | ) {} 145 | 146 | public function handle() 147 | { 148 | // perform some work... 149 | } 150 | } 151 | ``` 152 | 153 | Via artisan, you can call the job like this 154 | 155 | ```bash 156 | php artisan process-podcast --my-first-argument="My string value" 157 | ``` 158 | 159 | ### Using Eloquent models as arguments 160 | 161 | If your job argument is an eloquent model, you may pass the id of the model to the artisan command option. 162 | 163 | ```php 164 | use App\Models\Podcast; 165 | use Illuminate\Contracts\Queue\ShouldQueue; 166 | use Spatie\ArtisanDispatchable\Jobs\ArtisanDispatchable; 167 | 168 | class ProcessPodcast implements ShouldQueue, ArtisanDispatchable 169 | { 170 | public function __construct( 171 | Podcast $podcast, 172 | ) {} 173 | 174 | public function handle() 175 | { 176 | // perform some work... 177 | } 178 | } 179 | ``` 180 | 181 | Here's how you can execute this job with podcast id `1234` 182 | 183 | ```bash 184 | php artisan process-podcast --podcast="1234" 185 | ``` 186 | 187 | ### Customizing the name of the command 188 | 189 | By default, the artisan command name of a job, is the base name of job in kebab-case. 190 | 191 | You can set a custom name by setting a property named `artisanName` on your job. 192 | 193 | ```php 194 | use Illuminate\Contracts\Queue\ShouldQueue; 195 | use Spatie\ArtisanDispatchable\Jobs\ArtisanDispatchable; 196 | 197 | class ProcessPodcast implements ShouldQueue, ArtisanDispatchable 198 | { 199 | public string $artisanName = 'my-app:process-my-podcast'; 200 | 201 | public function handle() 202 | { 203 | // perform some work... 204 | } 205 | } 206 | ``` 207 | 208 | This job can now be executed with this command: 209 | 210 | ```bash 211 | php artisan my-app:process-my-podcast 212 | ``` 213 | 214 | ### Customizing the description of the command 215 | 216 | To add a description to the list of artisan commands, add a property `$artisanDescription` to your job. 217 | 218 | ```php 219 | use Illuminate\Contracts\Queue\ShouldQueue; 220 | use Spatie\ArtisanDispatchable\Jobs\ArtisanDispatchable; 221 | 222 | class ProcessPodcast implements ShouldQueue, ArtisanDispatchable 223 | { 224 | public string $artisanDescription = 'This a custom description'; 225 | 226 | public function handle() 227 | { 228 | // perform some work... 229 | } 230 | } 231 | ``` 232 | 233 | ### Prefixing all commands 234 | 235 | You can specify a prefix in the `command_name_prefix` key of the config file. When this is for example set to `my-custom-prefix`, then you would be able to call `MyDispatchableJob` with this command: 236 | 237 | ```bash 238 | php artisan my-custom-prefix:process-my-podcast 239 | ``` 240 | 241 | ### Caching discovered jobs 242 | 243 | This package can automatically discover jobs that implement `ArtisanDispatchable` and what their artisan command should be through looping through all classes and performing some reflection. In a local environment this is perfect, as the performance hit is not too bad, and you don't have to do anything special besides letting your job implement `ArtisanDispatchable`. 244 | 245 | In a production environment, you probably don't want to loop through all classes on every request. The package contains a command to cache all discovered jobs. 246 | 247 | ```bash 248 | php artisan artisan-dispatchable:cache-artisan-dispatchable-jobs 249 | ``` 250 | 251 | You probably want to call that command during your deployment of your app. This will create cache file at the location specified in the `cache_file` key of the `artisan-dispatchable` config file. 252 | 253 | Should you want to clear the cache, you can execute this command: 254 | 255 | ```bash 256 | php artisan artisan-dispatchable:clear-artisan-dispatchable-jobs 257 | ``` 258 | 259 | ## Testing 260 | 261 | ```bash 262 | composer test 263 | ``` 264 | 265 | ## Changelog 266 | 267 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 268 | 269 | ## Contributing 270 | 271 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 272 | 273 | ## Security Vulnerabilities 274 | 275 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 276 | 277 | ## Credits 278 | 279 | - [Freek Van der Herten](https://github.com/freekmurze) 280 | - [All Contributors](../../contributors) 281 | 282 | ## License 283 | 284 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 285 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-artisan-dispatchable", 3 | "description": "Dispatch Laravel jobs via Artisan", 4 | "keywords": [ 5 | "spatie", 6 | "laravel", 7 | "laravel-artisan-dispatchable" 8 | ], 9 | "homepage": "https://github.com/spatie/laravel-artisan-dispatchable", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Freek Van der Herten", 14 | "email": "freek@spatie.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "illuminate/console": "^10.0|^11.0|^12.0", 21 | "illuminate/contracts": "^10.0|^11.0|^12.0", 22 | "spatie/laravel-package-tools": "^1.16.3" 23 | }, 24 | "require-dev": { 25 | "brianium/paratest": "^6.2|^7.4.3", 26 | "nunomaduro/collision": "^6.0|^7.10|^8.0", 27 | "orchestra/testbench": "^8.21.1|^9.0|^10.0", 28 | "pestphp/pest": "^2.34.1|^3.7", 29 | "spatie/laravel-ray": "^1.35.1", 30 | "spatie/pest-plugin-snapshots": "^2.1", 31 | "spatie/phpunit-snapshot-assertions": "^5.1.5" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Spatie\\ArtisanDispatchable\\": "src", 36 | "Spatie\\ArtisanDispatchable\\Database\\Factories\\": "database/factories" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Tests\\": "tests", 42 | "Tests\\Database\\Factories\\": "tests/database/factories" 43 | } 44 | }, 45 | "scripts": { 46 | "test": "./vendor/bin/pest --no-coverage", 47 | "test-coverage": "vendor/bin/pest --coverage-html coverage" 48 | }, 49 | "config": { 50 | "sort-packages": true, 51 | "allow-plugins": { 52 | "pestphp/pest-plugin": true 53 | } 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Spatie\\ArtisanDispatchable\\ArtisanDispatchableServiceProvider" 59 | ] 60 | } 61 | }, 62 | "minimum-stability": "dev", 63 | "prefer-stable": true 64 | } 65 | -------------------------------------------------------------------------------- /config/artisan-dispatchable.php: -------------------------------------------------------------------------------- 1 | [ 9 | app()->path(), 10 | ], 11 | 12 | /* 13 | * This directory will be used as the base path when scanning 14 | * for dispatchable jobs. 15 | */ 16 | 'auto_discover_base_path' => base_path(), 17 | 18 | /* 19 | * In production, you likely don't want the package to auto-discover dispatchable 20 | * jobs every time Artisan is invoked. The package can cache discovered job. 21 | * 22 | * Here you can specify where the cache should be stored. 23 | */ 24 | 'cache_file' => storage_path('app/artisan-dispatchable/artisan-dispatchable-jobs.php'), 25 | 26 | /** 27 | * Here you can specify the prefix to be used for all dispatchable jobs. 28 | */ 29 | 'command_name_prefix' => '', 30 | ]; 31 | -------------------------------------------------------------------------------- /src/ArtisanDispatchableServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-artisan-dispatchable') 16 | ->hasConfigFile() 17 | ->hasCommands([ 18 | CacheArtisanDispatchableJobsCommand::class, 19 | ClearArtisanDispatchableJobsCommand::class, 20 | ]); 21 | } 22 | 23 | public function packageBooted() 24 | { 25 | (new ArtisanJobRepository())->registerAll(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ArtisanJob.php: -------------------------------------------------------------------------------- 1 | getCommandName()} {$this->getOptionString()}"; 22 | } 23 | 24 | protected function getCommandName(): string 25 | { 26 | if ($name = $this->getDefaultForProperty('artisanName')) { 27 | return $name; 28 | } 29 | 30 | $shortClassName = class_basename($this->jobClassName); 31 | 32 | $prefix = config('artisan-dispatchable.command_name_prefix'); 33 | $command = Str::of($shortClassName)->kebab()->beforeLast('-job'); 34 | 35 | return $prefix 36 | ? "{$prefix}:{$command}" 37 | : $command; 38 | } 39 | 40 | public function getCommandDescription(): string 41 | { 42 | return $this->getDefaultForProperty('artisanDescription') ?? "Execute job {$this->jobClassName}"; 43 | } 44 | 45 | protected function getOptionString(): string 46 | { 47 | $parameters = (new ReflectionClass($this->jobClassName)) 48 | ->getConstructor() 49 | ?->getParameters() ?? []; 50 | 51 | return collect($parameters) 52 | ->map(fn (ReflectionParameter $parameter) => $parameter->name) 53 | ->map(fn (string $argumentName) => '{--' . Str::camel($argumentName) . '=}') 54 | ->add('{--queued}') 55 | ->implode(' '); 56 | } 57 | 58 | public function handleCommand(ClosureCommand $command): void 59 | { 60 | $parameters = $this->constructorValues($command); 61 | 62 | $job = new $this->jobClassName(...$parameters); 63 | 64 | $command->option('queued') 65 | ? dispatch($job) 66 | : dispatch_sync($job); 67 | } 68 | 69 | protected function constructorValues(ClosureCommand $command): array 70 | { 71 | $parameters = (new ReflectionClass($this->jobClassName)) 72 | ->getConstructor() 73 | ?->getParameters(); 74 | 75 | if (is_null(($parameters))) { 76 | return []; 77 | } 78 | 79 | return collect($parameters) 80 | ->map(function (ReflectionParameter $parameter) use ($command) { 81 | $parameterName = $parameter->getName(); 82 | 83 | $value = $command->option($parameterName); 84 | 85 | if (is_null($value)) { 86 | if ($parameter->isDefaultValueAvailable()) { 87 | return $parameter->getDefaultValue(); 88 | } 89 | 90 | throw RequiredOptionMissing::make($this->getCommandName(), $parameterName); 91 | } 92 | 93 | $parameterType = $parameter->getType()?->getName(); 94 | 95 | if (is_a($parameterType, Model::class, true)) { 96 | $model = $parameterType::find($value); 97 | 98 | if (is_null($model)) { 99 | throw ModelNotFound::make($this->getCommandName(), $parameterName, $value); 100 | } 101 | 102 | $value = $model; 103 | } 104 | 105 | return $value; 106 | }) 107 | ->all(); 108 | } 109 | 110 | protected function getDefaultForProperty(string $name): mixed 111 | { 112 | $reflectionClass = new ReflectionClass($this->jobClassName); 113 | 114 | $defaultProperties = $reflectionClass->getDefaultProperties(); 115 | 116 | return $defaultProperties[$name] ?? null; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ArtisanJobRepository.php: -------------------------------------------------------------------------------- 1 | getAll() 15 | ->each(function (DiscoveredArtisanJob $discoveredArtisanJob) { 16 | $artisanJob = new ArtisanJob($discoveredArtisanJob->jobClassName); 17 | Artisan::command($discoveredArtisanJob->commandSignature, function () use ($artisanJob) { 18 | /** @var $this ClosureCommand */ 19 | $artisanJob->handleCommand($this); 20 | })->purpose($discoveredArtisanJob->commandDescription); 21 | }); 22 | } 23 | 24 | public function getAll(): Collection 25 | { 26 | $cachedDispatchableJobs = $this->getCachedDispatchableJobs(); 27 | 28 | if (! is_null($cachedDispatchableJobs)) { 29 | return collect($cachedDispatchableJobs) 30 | ->map(function (array $jobProperties) { 31 | return new DiscoveredArtisanJob(...$jobProperties); 32 | }); 33 | } 34 | 35 | return $this->getUnCachedDispatchableJobs(); 36 | } 37 | 38 | public function getUnCachedDispatchableJobs(): Collection 39 | { 40 | return (new DiscoverArtisanJobs()) 41 | ->within(config('artisan-dispatchable.auto_discover_dispatchable_jobs')) 42 | ->useBasePath(config('artisan-dispatchable.auto_discover_base_path')) 43 | ->ignoringFiles(Composer::getAutoloadedFiles(base_path('composer.json'))) 44 | ->getArtisanDispatchableJobs(); 45 | } 46 | 47 | protected function getCachedDispatchableJobs(): ?array 48 | { 49 | $cachedDispatchableJobs = config('artisan-dispatchable.cache_file'); 50 | 51 | if (! file_exists($cachedDispatchableJobs)) { 52 | return null; 53 | } 54 | 55 | return require $cachedDispatchableJobs; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Composer.php: -------------------------------------------------------------------------------- 1 | realpath($basePath.$path), $paths); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Console/CacheArtisanDispatchableJobsCommand.php: -------------------------------------------------------------------------------- 1 | info('Caching artisan dispatchable jobs...'); 19 | 20 | $artisanJobs = (new ArtisanJobRepository()) 21 | ->getUnCachedDispatchableJobs() 22 | ->map(fn (DiscoveredArtisanJob $discoveredArtisanJob) => $discoveredArtisanJob->toArray()) 23 | ->toArray(); 24 | 25 | $cachePath = config('artisan-dispatchable.cache_file'); 26 | 27 | $cacheDirectory = pathinfo($cachePath, PATHINFO_DIRNAME); 28 | 29 | $files->makeDirectory($cacheDirectory, 0755, true, true); 30 | 31 | $files->put( 32 | $cachePath, 33 | 'info('All done!'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Console/ClearArtisanDispatchableJobsCommand.php: -------------------------------------------------------------------------------- 1 | delete(config('artisan-dispatchable.cache_file')); 17 | 18 | $this->info('Cached artisan dispatchable jobs cleared!'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/DiscoverArtisanJobs.php: -------------------------------------------------------------------------------- 1 | basePath = app()->path(); 24 | } 25 | 26 | public function within(array $directories): self 27 | { 28 | $this->directories = $directories; 29 | 30 | return $this; 31 | } 32 | 33 | public function useBasePath(string $basePath): self 34 | { 35 | $this->basePath = $basePath; 36 | 37 | return $this; 38 | } 39 | 40 | public function useRootNamespace(string $rootNamespace): self 41 | { 42 | $this->rootNamespace = $rootNamespace; 43 | 44 | return $this; 45 | } 46 | 47 | public function ignoringFiles(array $ignoredFiles): self 48 | { 49 | $this->ignoredFiles = $ignoredFiles; 50 | 51 | return $this; 52 | } 53 | 54 | public function getArtisanDispatchableJobs(): Collection 55 | { 56 | if (empty($this->directories)) { 57 | return new Collection(); 58 | } 59 | 60 | $files = (new Finder())->files()->in($this->directories); 61 | 62 | return collect($files) 63 | ->reject(fn (SplFileInfo $file) => in_array($file->getPathname(), $this->ignoredFiles)) 64 | ->map(fn (SplFileInfo $file) => $this->fullQualifiedClassNameFromFile($file)) 65 | ->filter(function (string $eventHandlerClass) { 66 | return is_subclass_of($eventHandlerClass, ArtisanDispatchable::class); 67 | }) 68 | ->map(function (string $className) { 69 | $artisanJob = (new ArtisanJob($className)); 70 | 71 | return new DiscoveredArtisanJob( 72 | $className, 73 | $artisanJob->getFullCommand(), 74 | $artisanJob->getCommandDescription(), 75 | ); 76 | }) 77 | ->values(); 78 | } 79 | 80 | protected function fullQualifiedClassNameFromFile(SplFileInfo $file): string 81 | { 82 | return Str::of($file->getRealPath()) 83 | ->replaceFirst($this->basePath, '') 84 | ->trim(DIRECTORY_SEPARATOR) 85 | ->ucfirst() 86 | ->replaceLast('.php', '') 87 | ->replace([DIRECTORY_SEPARATOR, 'App\\'], ['\\', app()->getNamespace()]) 88 | ->prepend($this->rootNamespace); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/DiscoveredArtisanJob.php: -------------------------------------------------------------------------------- 1 | $this->jobClassName, 18 | 'commandSignature' => $this->commandSignature, 19 | 'commandDescription' => $this->commandDescription, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Exceptions/ModelNotFound.php: -------------------------------------------------------------------------------- 1 |