├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── check ├── composer.json └── src └── JobPipeline.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | ['syntax' => 'short'], 8 | 'binary_operator_spaces' => [ 9 | 'default' => 'single_space', 10 | 'operators' => [ 11 | '=>' => null, 12 | '|' => 'no_space', 13 | ], 14 | ], 15 | 'blank_line_after_namespace' => true, 16 | 'blank_line_after_opening_tag' => true, 17 | 'no_superfluous_phpdoc_tags' => true, 18 | 'blank_line_before_statement' => [ 19 | 'statements' => ['return'], 20 | ], 21 | 'braces' => true, 22 | 'cast_spaces' => true, 23 | 'class_definition' => true, 24 | 'concat_space' => [ 25 | 'spacing' => 'one', 26 | ], 27 | 'declare_equal_normalize' => true, 28 | 'elseif' => true, 29 | 'encoding' => true, 30 | 'full_opening_tag' => true, 31 | 'declare_strict_types' => true, 32 | 'fully_qualified_strict_types' => true, // added by Shift 33 | 'function_declaration' => true, 34 | 'function_typehint_space' => true, 35 | 'heredoc_to_nowdoc' => true, 36 | 'include' => true, 37 | 'increment_style' => ['style' => 'post'], 38 | 'indentation_type' => true, 39 | 'linebreak_after_opening_tag' => true, 40 | 'line_ending' => true, 41 | 'lowercase_cast' => true, 42 | 'constant_case' => true, 43 | 'lowercase_keywords' => true, 44 | 'lowercase_static_reference' => true, // added from Symfony 45 | 'magic_method_casing' => true, // added from Symfony 46 | 'magic_constant_casing' => true, 47 | 'method_argument_space' => true, 48 | 'native_function_casing' => true, 49 | 'no_alias_functions' => true, 50 | 'no_extra_blank_lines' => [ 51 | 'tokens' => [ 52 | 'extra', 53 | 'throw', 54 | 'use', 55 | 'use_trait', 56 | ], 57 | ], 58 | 'no_blank_lines_after_class_opening' => true, 59 | 'no_blank_lines_after_phpdoc' => true, 60 | 'no_closing_tag' => true, 61 | 'no_empty_phpdoc' => true, 62 | 'no_empty_statement' => true, 63 | 'no_leading_import_slash' => true, 64 | 'no_leading_namespace_whitespace' => true, 65 | 'no_mixed_echo_print' => [ 66 | 'use' => 'echo', 67 | ], 68 | 'no_multiline_whitespace_around_double_arrow' => true, 69 | 'multiline_whitespace_before_semicolons' => [ 70 | 'strategy' => 'no_multi_line', 71 | ], 72 | 'no_short_bool_cast' => true, 73 | 'no_singleline_whitespace_before_semicolons' => true, 74 | 'no_spaces_after_function_name' => true, 75 | 'no_spaces_around_offset' => true, 76 | 'no_spaces_inside_parenthesis' => true, 77 | 'no_trailing_comma_in_list_call' => true, 78 | 'no_trailing_comma_in_singleline_array' => true, 79 | 'no_trailing_whitespace' => true, 80 | 'no_trailing_whitespace_in_comment' => true, 81 | 'no_unneeded_control_parentheses' => true, 82 | 'no_unreachable_default_argument_value' => true, 83 | 'no_useless_return' => true, 84 | 'no_whitespace_before_comma_in_array' => true, 85 | 'no_whitespace_in_blank_line' => true, 86 | 'normalize_index_brace' => true, 87 | 'not_operator_with_successor_space' => true, 88 | 'object_operator_without_whitespace' => true, 89 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 90 | 'phpdoc_indent' => true, 91 | 'general_phpdoc_tag_rename' => true, 92 | 'phpdoc_no_access' => true, 93 | 'phpdoc_no_package' => true, 94 | 'phpdoc_no_useless_inheritdoc' => true, 95 | 'phpdoc_scalar' => true, 96 | 'phpdoc_single_line_var_spacing' => true, 97 | 'phpdoc_summary' => true, 98 | 'phpdoc_to_comment' => false, 99 | 'phpdoc_trim' => true, 100 | 'phpdoc_types' => true, 101 | 'phpdoc_var_without_name' => true, 102 | 'psr_autoloading' => true, 103 | 'self_accessor' => true, 104 | 'short_scalar_cast' => true, 105 | 'simplified_null_return' => false, // disabled by Shift 106 | 'single_blank_line_at_eof' => true, 107 | 'single_blank_line_before_namespace' => true, 108 | 'single_class_element_per_statement' => true, 109 | 'single_import_per_statement' => false, 110 | 'single_line_after_imports' => true, 111 | 'no_unused_imports' => true, 112 | 'single_line_comment_style' => [ 113 | 'comment_types' => ['hash'], 114 | ], 115 | 'single_quote' => true, 116 | 'space_after_semicolon' => true, 117 | 'standardize_not_equals' => true, 118 | 'switch_case_semicolon_to_colon' => true, 119 | 'switch_case_space' => true, 120 | 'ternary_operator_spaces' => true, 121 | 'trailing_comma_in_multiline' => true, 122 | 'trim_array_spaces' => true, 123 | 'unary_operator_spaces' => true, 124 | 'whitespace_after_comma_in_array' => true, 125 | ]; 126 | 127 | $project_path = getcwd(); 128 | $finder = Finder::create() 129 | ->in([ 130 | $project_path . '/src', 131 | ]) 132 | ->name('*.php') 133 | ->notName('*.blade.php') 134 | ->ignoreDotFiles(true) 135 | ->ignoreVCS(true); 136 | 137 | return (new Config()) 138 | ->setFinder($finder) 139 | ->setRules($rules) 140 | ->setRiskyAllowed(true) 141 | ->setUsingCache(true); 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Samuel Štancl 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Job Pipeline 2 | 3 |

4 | Job Pipeline 5 |

6 | 7 | The `JobPipeline` is a simple, yet **extremely powerful** class that lets you **convert any (series of) jobs into event listeners.** 8 | 9 | You may use a job pipeline like any other listener, so you can register it in the `EventServiceProvider` using the `$listen` array, or in any other place using `Event::listen()` — up to you. 10 | 11 | ## Creating job pipelines 12 | 13 | > These code snippets will use examples from [my multi-tenancy package](https://github.com/stancl/tenancy). 14 | 15 | To create a job pipeline, start by specifying the jobs you want to use: 16 | 17 | ```php 18 | send(function (TenantCreated $event) { 44 | return $event->tenant; 45 | }) 46 | ``` 47 | 48 | Next, decide if you want to queue the pipeline. By default, pipelines are synchronous (= not queued) by default. 49 | 50 | > 🔥 If you **do** want pipelines to be queued by default, you can do that by setting a static property: 51 | `\Stancl\JobPipeline\JobPipeline::$shouldBeQueuedByDefault = true;` 52 | 53 | ```php 54 | send(function (TenantCreated $event) { 65 | return $event->tenant; 66 | })->shouldBeQueued(true) 67 | ``` 68 | 69 | If you wish to push the job to a different queue, you can pass a string as the second parameter: 70 | 71 | ```php 72 | send(function (TenantCreated $event) { 83 | return $event->tenant; 84 | })->shouldBeQueued(true, 'another-queue'); 85 | ``` 86 | 87 | This can be simplified by calling `shouldBeQueued(queue: 'another-queue')` since the first parameter defaults to `true`. 88 | 89 | Finally, convert the pipeline to a listener and bind it to an event: 90 | 91 | ```php 92 | send(function (TenantCreated $event) { 104 | return $event->tenant; 105 | })->shouldBeQueued(true)->toListener()); 106 | ``` 107 | 108 | Note that you can use job pipelines even for converting single jobs to event listeners. That's useful if you have some logic in job classes and don't want to create listener classes just to be able to run these jobs as a result of an event being fired. 109 | 110 | Tip: Returning `false` from a job cancels the execution of all following jobs in the pipeline. This can be useful to cancel a job pipeline that creates, migrates, and seeds databases if the create database job exists (e.g. because it detects that a database already exists). So it can be good to separate jobs into multiple pipelines, so that each logical category of jobs can be stopped individually. 111 | -------------------------------------------------------------------------------- /check: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | offer_run() { 5 | read -p "For more output, run $1. Run it now (Y/n)? " run 6 | 7 | case ${run:0:1} in 8 | n|N ) 9 | exit 1 10 | ;; 11 | * ) 12 | $1 13 | ;; 14 | esac 15 | 16 | exit 1 17 | } 18 | 19 | if (php-cs-fixer fix --dry-run --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then 20 | echo '✅ php-cs-fixer OK' 21 | else 22 | read -p "⚠️ php-cs-fixer found issues. Fix (Y/n)? " fix 23 | case ${fix:0:1} in 24 | n|N ) 25 | echo '❌ php-cs-fixer FAIL' 26 | offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php' 27 | ;; 28 | * ) 29 | if (php-cs-fixer fix --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then 30 | echo '✅ php-cs-fixer OK' 31 | else 32 | echo '❌ php-cs-fixer FAIL' 33 | offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php' 34 | fi 35 | ;; 36 | esac 37 | fi 38 | 39 | if (./vendor/bin/phpunit > /dev/null 2>/dev/null); then 40 | echo '✅ PHPUnit OK' 41 | else 42 | echo '❌ PHPUnit FAIL' 43 | offer_run './vendor/bin/phpunit' 44 | fi 45 | 46 | echo '==================' 47 | echo '✅ Everything OK' 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stancl/jobpipeline", 3 | "description": "Turn any series of jobs into Laravel listeners.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Samuel Štancl", 8 | "email": "samuel.stancl@gmail.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Stancl\\JobPipeline\\": "src/" 14 | } 15 | }, 16 | "autoload-dev": { 17 | "psr-4": { 18 | "Stancl\\JobPipeline\\Tests\\": "tests/" 19 | } 20 | }, 21 | "require": { 22 | "php": "^8.0", 23 | "illuminate/support": "^10.0|^11.0|^12.0" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^8.0|^9.0|^10.0", 27 | "spatie/valuestore": "^1.2", 28 | "ext-redis": "*" 29 | }, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true 32 | } 33 | -------------------------------------------------------------------------------- /src/JobPipeline.php: -------------------------------------------------------------------------------- 1 | jobs = $jobs; 34 | $this->send = $send ?? function ($event) { 35 | // If no $send callback is set, we'll just pass the event through the jobs. 36 | return $event; 37 | }; 38 | $this->shouldBeQueued = $shouldBeQueued ?? static::$shouldBeQueuedByDefault; 39 | } 40 | 41 | /** @param callable[]|string[] $jobs */ 42 | public static function make(array $jobs): self 43 | { 44 | return new static($jobs); 45 | } 46 | 47 | public function send(callable $send): self 48 | { 49 | $this->send = $send; 50 | 51 | return $this; 52 | } 53 | 54 | public function shouldBeQueued(bool $shouldBeQueued = true, string $queue = null) 55 | { 56 | $this->shouldBeQueued = $shouldBeQueued; 57 | 58 | if ($queue) { 59 | $this->queue = $queue; 60 | } 61 | 62 | return $this; 63 | } 64 | 65 | public function handle(): void 66 | { 67 | foreach ($this->jobs as $job) { 68 | if (is_string($job)) { 69 | $job = [new $job(...$this->passable), 'handle']; 70 | } 71 | 72 | try { 73 | $result = app()->call($job); 74 | } catch (Throwable $exception) { 75 | if (is_array($job) && method_exists(get_class($job[0]), 'failed')) { 76 | call_user_func_array([$job[0], 'failed'], [$exception]); 77 | } else { 78 | throw $exception; 79 | } 80 | 81 | break; 82 | } 83 | 84 | if ($result === false) { 85 | break; 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * Generate a closure that can be used as a listener. 92 | */ 93 | public function toListener(): Closure 94 | { 95 | return function (...$args) { 96 | $executable = $this->executable($args); 97 | 98 | if ($this->shouldBeQueued) { 99 | dispatch($executable); 100 | } else { 101 | dispatch_sync($executable); 102 | } 103 | }; 104 | } 105 | 106 | /** 107 | * Return a serializable version of the current object. 108 | */ 109 | public function executable($listenerArgs): self 110 | { 111 | $clone = clone $this; 112 | 113 | $passable = ($clone->send)(...$listenerArgs); 114 | $passable = is_array($passable) ? $passable : [$passable]; 115 | 116 | $clone->passable = $passable; 117 | $clone->send = null; 118 | 119 | return $clone; 120 | } 121 | } 122 | --------------------------------------------------------------------------------