├── .php_cs.dist.php ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Action.php ├── ActionManager.php ├── ActionPendingChain.php ├── ActionRequest.php ├── ActionServiceProvider.php ├── AttributeValidator.php ├── BacktraceFrame.php ├── Concerns ├── AsAction.php ├── AsCommand.php ├── AsController.php ├── AsFake.php ├── AsJob.php ├── AsListener.php ├── AsObject.php ├── DecorateActions.php ├── ValidateActions.php └── WithAttributes.php ├── Console ├── MakeActionCommand.php └── stubs │ └── action.stub ├── Decorators ├── CommandDecorator.php ├── ControllerDecorator.php ├── JobDecorator.php ├── ListenerDecorator.php └── UniqueJobDecorator.php ├── DesignPatterns ├── CommandDesignPattern.php ├── ControllerDesignPattern.php ├── DesignPattern.php └── ListenerDesignPattern.php ├── Exceptions └── MissingCommandSignatureException.php └── Facades └── Actions.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 | 'method_argument_space' => [ 30 | 'on_multiline' => 'ensure_fully_multiline', 31 | 'keep_multiple_spaces_after_comma' => true, 32 | ], 33 | 'single_trait_insert_per_statement' => true, 34 | ]) 35 | ->setFinder($finder); 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Loris Leiva 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 | # ⚡️ Laravel Actions 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/lorisleiva/laravel-actions.svg)](https://packagist.org/packages/lorisleiva/laravel-actions) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/lorisleiva/laravel-actions/run-tests.yml?branch=main)](https://github.com/lorisleiva/laravel-actions/actions?query=workflow%3ATests+branch%3Amain) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/lorisleiva/laravel-actions.svg)](https://packagist.org/packages/lorisleiva/laravel-actions) 6 | 7 | ![hero](https://user-images.githubusercontent.com/3642397/104024620-4e572400-51bb-11eb-97fc-c2692b16eaa7.png) 8 | 9 | ⚡ **Classes that take care of one specific task.** 10 | 11 | This package introduces a new way of organising the logic of your Laravel applications by focusing on the actions your applications provide. 12 | 13 | Instead of creating controllers, jobs, listeners and so on, it allows you to create a PHP class that handles a specific task and run that class as anything you want. 14 | 15 | Therefore it encourages you to switch your focus from: 16 | 17 | > "What controllers do I need?", "should I make a FormRequest for this?", "should this run asynchronously in a job instead?", etc. 18 | 19 | to: 20 | 21 | > "What does my application actually do?" 22 | 23 | ## Installation 24 | 25 | ```bash 26 | composer require lorisleiva/laravel-actions 27 | ``` 28 | 29 | ## Documentation 30 | 31 | :books: Read the full documentation at [laravelactions.com](https://laravelactions.com/) 32 | 33 | ## Basic usage 34 | 35 | Create your first action using `php artisan make:action PublishANewArticle` and define the `asX` methods when you want your action to be running as `X`. E.g. `asController`, `asJob`, `asListener` and/or `asCommand`. 36 | 37 | ``` php 38 | class PublishANewArticle 39 | { 40 | use AsAction; 41 | 42 | public function handle(User $author, string $title, string $body): Article 43 | { 44 | return $author->articles()->create([ 45 | 'title' => $title, 46 | 'body' => $body, 47 | ]); 48 | } 49 | 50 | public function asController(Request $request): ArticleResource 51 | { 52 | $article = $this->handle( 53 | $request->user(), 54 | $request->get('title'), 55 | $request->get('body'), 56 | ); 57 | 58 | return new ArticleResource($article); 59 | } 60 | 61 | public function asListener(NewProductReleased $event): void 62 | { 63 | $this->handle( 64 | $event->product->manager, 65 | $event->product->name . ' Released!', 66 | $event->product->description, 67 | ); 68 | } 69 | } 70 | ``` 71 | 72 | ### As an object 73 | 74 | Now, you can run your action as an object by using the `run` method like so: 75 | 76 | ```php 77 | PublishANewArticle::run($author, 'My title', 'My content'); 78 | ``` 79 | 80 | ### As a controller 81 | 82 | Simply register your action as an invokable controller in a routes file. 83 | 84 | ```php 85 | Route::post('articles', PublishANewArticle::class)->middleware('auth'); 86 | ``` 87 | 88 | ### As a listener 89 | 90 | Simply register your action as a listener of the `NewProductReleased` event. 91 | 92 | ```php 93 | Event::listen(NewProductReleased::class, PublishANewArticle::class); 94 | ``` 95 | 96 | Then, the `asListener` method of your action will be called whenever the `NewProductReleased` event is dispatched. 97 | 98 | ```php 99 | event(new NewProductReleased($manager, 'Product title', 'Product description')); 100 | ``` 101 | 102 | ### And more... 103 | 104 | On top of running your actions as objects, controllers and listeners, Laravel Actions also supports jobs, commands and even mocking your actions in tests. 105 | 106 | 📚 [Check out the full documentation to learn everything that Laravel Actions has to offer](https://laravelactions.com/). 107 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lorisleiva/laravel-actions", 3 | "description": "Laravel components that take care of one specific task", 4 | "keywords": [ 5 | "laravel", 6 | "component", 7 | "action", 8 | "controller", 9 | "job", 10 | "object", 11 | "command", 12 | "listener" 13 | ], 14 | "homepage": "https://github.com/lorisleiva/laravel-actions", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Loris Leiva", 19 | "email": "loris.leiva@gmail.com", 20 | "homepage": "https://lorisleiva.com", 21 | "role": "Developer" 22 | } 23 | ], 24 | "require": { 25 | "php": "^8.1", 26 | "illuminate/contracts": "^10.0|^11.0|^12.0", 27 | "lorisleiva/lody": "^0.6" 28 | }, 29 | "require-dev": { 30 | "orchestra/testbench": "^10.0", 31 | "pestphp/pest": "^2.34|^3.0", 32 | "phpunit/phpunit": "^10.5|^11.5" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Lorisleiva\\Actions\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Lorisleiva\\Actions\\Tests\\": "tests" 42 | } 43 | }, 44 | "scripts": { 45 | "test": "vendor/bin/pest --colors=always", 46 | "test-coverage": "vendor/bin/pest --coverage-html coverage" 47 | }, 48 | "config": { 49 | "sort-packages": true, 50 | "allow-plugins": { 51 | "pestphp/pest-plugin": true 52 | } 53 | }, 54 | "extra": { 55 | "laravel": { 56 | "providers": [ 57 | "Lorisleiva\\Actions\\ActionServiceProvider" 58 | ], 59 | "aliases": { 60 | "Action": "Lorisleiva\\Actions\\Facades\\Actions" 61 | } 62 | } 63 | }, 64 | "minimum-stability": "dev", 65 | "prefer-stable": true, 66 | "funding": [ 67 | { 68 | "type": "github", 69 | "url": "https://github.com/sponsors/lorisleiva" 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /src/Action.php: -------------------------------------------------------------------------------- 1 | */ 20 | public static string $jobDecorator = JobDecorator::class; 21 | 22 | /** @var class-string */ 23 | public static string $uniqueJobDecorator = UniqueJobDecorator::class; 24 | 25 | /** @var DesignPattern[] */ 26 | protected array $designPatterns = []; 27 | 28 | /** @var bool[] */ 29 | protected array $extended = []; 30 | 31 | protected int $backtraceLimit = 10; 32 | 33 | public function __construct(array $designPatterns = []) 34 | { 35 | $this->setDesignPatterns($designPatterns); 36 | } 37 | 38 | /** 39 | * @param class-string $jobDecoratorClass 40 | */ 41 | public static function useJobDecorator(string $jobDecoratorClass): void 42 | { 43 | static::$jobDecorator = $jobDecoratorClass; 44 | } 45 | 46 | /** 47 | * @param class-string $uniqueJobDecoratorClass 48 | */ 49 | public static function useUniqueJobDecorator(string $uniqueJobDecoratorClass): void 50 | { 51 | static::$uniqueJobDecorator = $uniqueJobDecoratorClass; 52 | } 53 | 54 | public function setBacktraceLimit(int $backtraceLimit): ActionManager 55 | { 56 | $this->backtraceLimit = $backtraceLimit; 57 | 58 | return $this; 59 | } 60 | 61 | public function setDesignPatterns(array $designPatterns): ActionManager 62 | { 63 | $this->designPatterns = $designPatterns; 64 | 65 | return $this; 66 | } 67 | 68 | public function getDesignPatterns(): array 69 | { 70 | return $this->designPatterns; 71 | } 72 | 73 | public function registerDesignPattern(DesignPattern $designPattern): ActionManager 74 | { 75 | $this->designPatterns[] = $designPattern; 76 | 77 | return $this; 78 | } 79 | 80 | public function getDesignPatternsMatching(array $usedTraits): array 81 | { 82 | $filter = function (DesignPattern $designPattern) use ($usedTraits) { 83 | return in_array($designPattern->getTrait(), $usedTraits); 84 | }; 85 | 86 | return array_filter($this->getDesignPatterns(), $filter); 87 | } 88 | 89 | public function extend(Application $app, string $abstract): void 90 | { 91 | if ($this->isExtending($abstract)) { 92 | return; 93 | } 94 | 95 | if (! $this->shouldExtend($abstract)) { 96 | return; 97 | } 98 | 99 | $app->extend($abstract, function ($instance) { 100 | return $this->identifyAndDecorate($instance); 101 | }); 102 | 103 | $this->extended[$abstract] = true; 104 | } 105 | 106 | public function isExtending(string $abstract): bool 107 | { 108 | return isset($this->extended[$abstract]); 109 | } 110 | 111 | public function shouldExtend(string $abstract): bool 112 | { 113 | $usedTraits = class_uses_recursive($abstract); 114 | 115 | return ! empty($this->getDesignPatternsMatching($usedTraits)) 116 | || in_array(AsFake::class, $usedTraits); 117 | } 118 | 119 | public function identifyAndDecorate($instance) 120 | { 121 | $usedTraits = class_uses_recursive($instance); 122 | 123 | if (in_array(AsFake::class, $usedTraits) && $instance::isFake()) { 124 | $instance = $instance::mock(); 125 | } 126 | 127 | if (! $designPattern = $this->identifyFromBacktrace($usedTraits, $frame)) { 128 | return $instance; 129 | } 130 | 131 | return $designPattern->decorate($instance, $frame); 132 | } 133 | 134 | public function identifyFromBacktrace($usedTraits, ?BacktraceFrame &$frame = null): ?DesignPattern 135 | { 136 | $designPatterns = $this->getDesignPatternsMatching($usedTraits); 137 | $backtraceOptions = DEBUG_BACKTRACE_PROVIDE_OBJECT 138 | | DEBUG_BACKTRACE_IGNORE_ARGS; 139 | 140 | $ownNumberOfFrames = 2; 141 | $frames = array_slice( 142 | debug_backtrace($backtraceOptions, $ownNumberOfFrames + $this->backtraceLimit), 143 | $ownNumberOfFrames 144 | ); 145 | foreach ($frames as $frame) { 146 | $frame = new BacktraceFrame($frame); 147 | 148 | /** @var DesignPattern $designPattern */ 149 | foreach ($designPatterns as $designPattern) { 150 | if ($designPattern->recognizeFrame($frame)) { 151 | return $designPattern; 152 | } 153 | } 154 | } 155 | 156 | return null; 157 | } 158 | 159 | public function registerRoutes(array | string $paths = 'app/Actions'): void 160 | { 161 | Lody::classes($paths) 162 | ->isNotAbstract() 163 | ->hasTrait(AsController::class) 164 | ->hasStaticMethod('routes') 165 | ->each(fn (string $classname) => $this->registerRoutesForAction($classname)); 166 | } 167 | 168 | public function registerCommands(array | string $paths = 'app/Actions'): void 169 | { 170 | Lody::classes($paths) 171 | ->isNotAbstract() 172 | ->hasTrait(AsCommand::class) 173 | ->filter(function (string $classname): bool { 174 | return property_exists($classname, 'commandSignature') 175 | || method_exists($classname, 'getCommandSignature'); 176 | }) 177 | ->each(fn (string $classname) => $this->registerCommandsForAction($classname)); 178 | } 179 | 180 | public function registerRoutesForAction(string $className): void 181 | { 182 | $className::routes(app(Router::class)); 183 | } 184 | 185 | public function registerCommandsForAction(string $className): void 186 | { 187 | Artisan::starting(function ($artisan) use ($className) { 188 | $artisan->resolve($className); 189 | }); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/ActionPendingChain.php: -------------------------------------------------------------------------------- 1 | usesAsJobTrait($job = $this->job)) { 15 | $this->job = $job::makeJob(...func_get_args()); 16 | } 17 | 18 | return parent::dispatch(); 19 | } 20 | 21 | public function usesAsJobTrait($job): bool 22 | { 23 | return is_string($job) 24 | && class_exists($job) 25 | && in_array(AsJob::class, class_uses_recursive($job)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ActionRequest.php: -------------------------------------------------------------------------------- 1 | all(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ActionServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->scoped(ActionManager::class, function () { 20 | return new ActionManager([ 21 | new ControllerDesignPattern(), 22 | new ListenerDesignPattern(), 23 | new CommandDesignPattern(), 24 | ]); 25 | }); 26 | 27 | $this->extendActions(); 28 | } 29 | 30 | /** 31 | * Bootstrap any application services. 32 | */ 33 | public function boot(): void 34 | { 35 | if ($this->app->runningInConsole()) { 36 | // Publish Stubs File 37 | $this->publishes([ 38 | __DIR__ . '/Console/stubs/action.stub' => base_path('stubs/action.stub'), 39 | ], 'stubs'); 40 | 41 | // Register the make:action generator command. 42 | $this->commands([ 43 | MakeActionCommand::class, 44 | ]); 45 | } 46 | } 47 | 48 | protected function extendActions(): void 49 | { 50 | $this->app->beforeResolving(function ($abstract, $parameters, Application $app) { 51 | if ($abstract === ActionManager::class) { 52 | return; 53 | } 54 | 55 | try { 56 | // Fix conflict with package: barryvdh/laravel-ide-helper. 57 | // @see https://github.com/lorisleiva/laravel-actions/issues/142 58 | $classExists = class_exists($abstract); 59 | } catch (\ReflectionException) { 60 | return; 61 | } 62 | 63 | if (! $classExists || $app->resolved($abstract)) { 64 | return; 65 | } 66 | 67 | $app->make(ActionManager::class)->extend($app, $abstract); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/AttributeValidator.php: -------------------------------------------------------------------------------- 1 | setAction($action); 15 | $this->redirector = app(Redirector::class); 16 | } 17 | 18 | public static function for($action): self 19 | { 20 | return new static($action); 21 | } 22 | 23 | public function getDefaultValidationData(): array 24 | { 25 | return $this->fromActionMethod('all'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/BacktraceFrame.php: -------------------------------------------------------------------------------- 1 | class = Arr::get($frame, 'class'); 17 | $this->function = Arr::get($frame, 'function'); 18 | $this->isStatic = Arr::get($frame, 'type') === '::'; 19 | $this->object = Arr::get($frame, 'object'); 20 | } 21 | 22 | public function fromClass(): bool 23 | { 24 | return ! is_null($this->class); 25 | } 26 | 27 | public function instanceOf(string $superClass): bool 28 | { 29 | if (! $this->fromClass()) { 30 | return false; 31 | } 32 | 33 | return $this->class === $superClass 34 | || is_subclass_of($this->class, $superClass); 35 | } 36 | 37 | public function matches(string $class, string $method, ?bool $isStatic = null): bool 38 | { 39 | $matchesStatic = is_null($isStatic) || $this->isStatic === $isStatic; 40 | 41 | return $this->instanceOf($class) 42 | && $this->function === $method 43 | && $matchesStatic; 44 | } 45 | 46 | public function getObject() 47 | { 48 | return $this->object; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Concerns/AsAction.php: -------------------------------------------------------------------------------- 1 | handle(...$arguments); 20 | } 21 | 22 | /** 23 | * This empty method is required to enable controller middleware on the action. 24 | * @see https://github.com/lorisleiva/laravel-actions/issues/199 25 | */ 26 | public function getMiddleware(): array 27 | { 28 | return []; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Concerns/AsFake.php: -------------------------------------------------------------------------------- 1 | shouldAllowMockingProtectedMethods(); 21 | 22 | return static::setFakeResolvedInstance($mock); 23 | } 24 | 25 | public static function spy(): MockInterface 26 | { 27 | if (static::isFake()) { 28 | return static::getFakeResolvedInstance(); 29 | } 30 | 31 | return static::setFakeResolvedInstance(Mockery::spy(static::class)); 32 | } 33 | 34 | public static function partialMock(): MockInterface 35 | { 36 | return static::mock()->makePartial(); 37 | } 38 | 39 | public static function shouldRun(): Expectation|ExpectationInterface|HigherOrderMessage 40 | { 41 | return static::mock()->shouldReceive('handle'); 42 | } 43 | 44 | public static function shouldNotRun(): Expectation|ExpectationInterface|HigherOrderMessage 45 | { 46 | return static::mock()->shouldNotReceive('handle'); 47 | } 48 | 49 | public static function allowToRun(): Expectation|ExpectationInterface|HigherOrderMessage|MockInterface 50 | { 51 | return static::spy()->allows('handle'); 52 | } 53 | 54 | public static function isFake(): bool 55 | { 56 | return app()->isShared(static::getFakeResolvedInstanceKey()); 57 | } 58 | 59 | /** 60 | * Removes the fake instance from the container. 61 | */ 62 | public static function clearFake(): void 63 | { 64 | app()->forgetInstance(static::getFakeResolvedInstanceKey()); 65 | } 66 | 67 | protected static function setFakeResolvedInstance(MockInterface $fake): MockInterface 68 | { 69 | return app()->instance(static::getFakeResolvedInstanceKey(), $fake); 70 | } 71 | 72 | protected static function getFakeResolvedInstance(): ?MockInterface 73 | { 74 | return app(static::getFakeResolvedInstanceKey()); 75 | } 76 | 77 | protected static function getFakeResolvedInstanceKey(): string 78 | { 79 | return 'LaravelActions:AsFake:' . static::class; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Concerns/AsJob.php: -------------------------------------------------------------------------------- 1 | dispatchSync(static::makeJob(...$arguments)); 91 | } 92 | 93 | public static function dispatchNow(mixed ...$arguments): mixed 94 | { 95 | return static::dispatchSync(...$arguments); 96 | } 97 | 98 | public static function dispatchAfterResponse(mixed ...$arguments): void 99 | { 100 | static::dispatch(...$arguments)->afterResponse(); 101 | } 102 | 103 | public static function withChain(array $chain): ActionPendingChain 104 | { 105 | return new ActionPendingChain(static::class, $chain); 106 | } 107 | 108 | public static function assertPushed(Closure|int|null $times = null, Closure|null $callback = null): void 109 | { 110 | if ($times instanceof Closure) { 111 | $callback = $times; 112 | $times = null; 113 | } 114 | 115 | $decoratorClass = static::jobShouldBeUnique() 116 | ? ActionManager::$uniqueJobDecorator 117 | : ActionManager::$jobDecorator; 118 | 119 | $count = Queue::pushed($decoratorClass, function (JobDecorator $job, $queue) use ($callback) { 120 | if (! $job->decorates(static::class)) { 121 | return false; 122 | } 123 | 124 | if (! $callback) { 125 | return true; 126 | } 127 | 128 | return $callback($job->getAction(), $job->getParameters(), $job, $queue); 129 | })->count(); 130 | 131 | $job = static::class; 132 | 133 | if (is_null($times)) { 134 | PHPUnit::assertTrue( 135 | $count > 0, 136 | "The expected [{$job}] job was not pushed." 137 | ); 138 | } elseif ($times === 0) { 139 | PHPUnit::assertTrue( 140 | $count === 0, 141 | "The unexpected [{$job}] job was pushed." 142 | ); 143 | } else { 144 | PHPUnit::assertSame( 145 | $times, 146 | $count, 147 | "The expected [{$job}] job was pushed {$count} times instead of {$times} times." 148 | ); 149 | } 150 | } 151 | 152 | public static function assertNotPushed(Closure|null $callback = null): void 153 | { 154 | static::assertPushed(0, $callback); 155 | } 156 | 157 | public static function assertPushedOn(string $queue, Closure|int|null $times = null, Closure|null $callback = null): void 158 | { 159 | if ($times instanceof Closure) { 160 | $callback = $times; 161 | $times = null; 162 | } 163 | 164 | static::assertPushed($times, function ($action, $parameters, $job, $pushedQueue) use ($callback, $queue) { 165 | if ($pushedQueue !== $queue) { 166 | return false; 167 | } 168 | 169 | return $callback ? $callback(...func_get_args()) : true; 170 | }); 171 | } 172 | 173 | public static function assertPushedWith(Closure|array $callback, ?string $queue = null): void 174 | { 175 | if (is_array($callback)) { 176 | $callback = fn (...$params) => $params === $callback; 177 | } 178 | 179 | static::assertPushed( 180 | fn ($action, $params, JobDecorator $job, $q) => $callback(...$params) && (is_null($queue) || $q === $queue) 181 | ); 182 | } 183 | 184 | public static function assertNotPushedWith(Closure|array $callback): void 185 | { 186 | if (is_array($callback)) { 187 | $callback = fn (...$params) => $params === $callback; 188 | } 189 | 190 | static::assertNotPushed(fn ($action, $params, JobDecorator $job) => $callback(...$job->getParameters())); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Concerns/AsListener.php: -------------------------------------------------------------------------------- 1 | handle(...$arguments); 23 | } 24 | 25 | public static function runIf(bool $boolean, mixed ...$arguments): mixed 26 | { 27 | return $boolean ? static::run(...$arguments) : new Fluent; 28 | } 29 | 30 | public static function runUnless(bool $boolean, mixed ...$arguments): mixed 31 | { 32 | return static::runIf(! $boolean, ...$arguments); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Concerns/DecorateActions.php: -------------------------------------------------------------------------------- 1 | action = $action; 12 | 13 | return $this; 14 | } 15 | 16 | protected function hasTrait(string $trait): bool 17 | { 18 | return in_array($trait, class_uses_recursive($this->action)); 19 | } 20 | 21 | protected function hasProperty(string $property): bool 22 | { 23 | return property_exists($this->action, $property); 24 | } 25 | 26 | protected function getProperty(string $property) 27 | { 28 | return $this->action->{$property}; 29 | } 30 | 31 | protected function hasMethod(string $method): bool 32 | { 33 | return method_exists($this->action, $method); 34 | } 35 | 36 | protected function callMethod(string $method, array $parameters = []) 37 | { 38 | return call_user_func_array([$this->action, $method], $parameters); 39 | } 40 | 41 | protected function resolveAndCallMethod(string $method, array $extraArguments = []) 42 | { 43 | return app()->call([$this->action, $method], $extraArguments); 44 | } 45 | 46 | protected function fromActionMethod(string $method, array $methodParameters = [], $default = null) 47 | { 48 | return $this->hasMethod($method) 49 | ? $this->callMethod($method, $methodParameters) 50 | : value($default); 51 | } 52 | 53 | protected function fromActionProperty(string $property, $default = null) 54 | { 55 | return $this->hasProperty($property) 56 | ? $this->getProperty($property) 57 | : value($default); 58 | } 59 | 60 | protected function fromActionMethodOrProperty(string $method, string $property, $default = null, array $methodParameters = []) 61 | { 62 | if ($this->hasMethod($method)) { 63 | return $this->callMethod($method, $methodParameters); 64 | } 65 | 66 | if ($this->hasProperty($property)) { 67 | return $this->getProperty($property); 68 | } 69 | 70 | return value($default); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Concerns/ValidateActions.php: -------------------------------------------------------------------------------- 1 | prepareForValidation(); 28 | $response = $this->inspectAuthorization(); 29 | 30 | if (! $response->allowed()) { 31 | $this->deniedAuthorization($response); 32 | } 33 | 34 | $instance = $this->getValidatorInstance(); 35 | 36 | if ($instance->fails()) { 37 | $this->failedValidation($instance); 38 | } 39 | } 40 | 41 | protected function getValidatorInstance(): Validator 42 | { 43 | if ($this->validator) { 44 | return $this->validator; 45 | } 46 | 47 | $factory = app(ValidationFactory::class); 48 | 49 | if ($this->hasMethod('getValidator')) { 50 | $validator = $this->resolveAndCallMethod('getValidator', compact('factory')); 51 | } else { 52 | $validator = $this->createDefaultValidator($factory); 53 | } 54 | 55 | if ($this->hasMethod('withValidator')) { 56 | $this->resolveAndCallMethod('withValidator', compact('validator')); 57 | } 58 | 59 | if ($this->hasMethod('afterValidator')) { 60 | $validator->after(function ($validator) { 61 | $this->resolveAndCallMethod('afterValidator', compact('validator')); 62 | }); 63 | } 64 | 65 | return $this->validator = $validator; 66 | } 67 | 68 | protected function createDefaultValidator(ValidationFactory $factory): Validator 69 | { 70 | return $factory->make( 71 | $this->validationData(), 72 | $this->rules(), 73 | $this->messages(), 74 | $this->attributes() 75 | ); 76 | } 77 | 78 | public function validationData(): array 79 | { 80 | return $this->hasMethod('getValidationData') 81 | ? $this->resolveAndCallMethod('getValidationData') 82 | : $this->getDefaultValidationData(); 83 | } 84 | 85 | public function rules(): array 86 | { 87 | return $this->hasMethod('rules') 88 | ? $this->resolveAndCallMethod('rules') 89 | : []; 90 | } 91 | 92 | public function messages(): array 93 | { 94 | return $this->hasMethod('getValidationMessages') 95 | ? $this->resolveAndCallMethod('getValidationMessages') 96 | : []; 97 | } 98 | 99 | public function attributes(): array 100 | { 101 | return $this->hasMethod('getValidationAttributes') 102 | ? $this->resolveAndCallMethod('getValidationAttributes') 103 | : []; 104 | } 105 | 106 | protected function failedValidation(Validator $validator) 107 | { 108 | if ($this->hasMethod('getValidationFailure')) { 109 | return $this->resolveAndCallMethod('getValidationFailure', compact('validator')); 110 | } 111 | 112 | throw (new ValidationException($validator)) 113 | ->errorBag($this->getErrorBag($validator)) 114 | ->redirectTo($this->getRedirectUrl()); 115 | } 116 | 117 | protected function getRedirectUrl() 118 | { 119 | $url = $this->redirector->getUrlGenerator(); 120 | 121 | return $this->hasMethod('getValidationRedirect') 122 | ? $this->resolveAndCallMethod('getValidationRedirect', compact('url')) 123 | : $url->previous(); 124 | } 125 | 126 | protected function getErrorBag(Validator $validator): string 127 | { 128 | return $this->hasMethod('getValidationErrorBag') 129 | ? $this->resolveAndCallMethod('getValidationErrorBag', compact('validator')) 130 | : 'default'; 131 | } 132 | 133 | protected function inspectAuthorization(): Response 134 | { 135 | try { 136 | $response = $this->hasMethod('authorize') 137 | ? $this->resolveAndCallMethod('authorize') 138 | : true; 139 | } catch (AuthorizationException $e) { 140 | return $e->toResponse(); 141 | } 142 | 143 | if ($response instanceof Response) { 144 | return $response; 145 | } 146 | 147 | return $response ? Response::allow() : Response::deny(); 148 | } 149 | 150 | protected function deniedAuthorization(Response $response): void 151 | { 152 | if ($this->hasMethod('getAuthorizationFailure')) { 153 | $this->resolveAndCallMethod('getAuthorizationFailure', compact('response')); 154 | 155 | return; 156 | } 157 | 158 | $response->authorize(); 159 | } 160 | 161 | public function validated($key = null, $default = null): mixed 162 | { 163 | return data_get($this->validator->validated(), $key, $default); 164 | } 165 | 166 | protected function prepareForValidation() 167 | { 168 | if ($this->hasMethod('prepareForValidation')) { 169 | return $this->resolveAndCallMethod('prepareForValidation'); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Concerns/WithAttributes.php: -------------------------------------------------------------------------------- 1 | attributes = $attributes; 31 | 32 | return $this; 33 | } 34 | 35 | public function fill(array $attributes): static 36 | { 37 | $this->attributes = array_merge($this->attributes, $attributes); 38 | 39 | return $this; 40 | } 41 | 42 | public function fillFromRequest(Request $request): static 43 | { 44 | $route = $request->route(); 45 | 46 | $this->attributes = array_merge( 47 | $this->attributes, 48 | $route ? $route->parametersWithoutNulls() : [], 49 | $request->all(), 50 | ); 51 | 52 | return $this; 53 | } 54 | 55 | public function all(): array 56 | { 57 | return $this->attributes; 58 | } 59 | 60 | public function only($keys): array 61 | { 62 | return Arr::only($this->attributes, is_array($keys) ? $keys : func_get_args()); 63 | } 64 | 65 | public function except($keys): array 66 | { 67 | return Arr::except($this->attributes, is_array($keys) ? $keys : func_get_args()); 68 | } 69 | 70 | public function has($key): bool 71 | { 72 | return Arr::has($this->attributes, $key); 73 | } 74 | 75 | public function get($key, $default = null) 76 | { 77 | return Arr::get($this->attributes, $key, $default); 78 | } 79 | 80 | public function set(string $key, mixed $value): static 81 | { 82 | Arr::set($this->attributes, $key, $value); 83 | 84 | return $this; 85 | } 86 | 87 | public function __get($key) 88 | { 89 | return $this->get($key); 90 | } 91 | 92 | public function __set($key, $value) 93 | { 94 | $this->set($key, $value); 95 | } 96 | 97 | public function __isset($key): bool 98 | { 99 | return ! is_null($this->get($key)); 100 | } 101 | 102 | public function validateAttributes(): array 103 | { 104 | $validator = AttributeValidator::for($this); 105 | $validator->validate(); 106 | 107 | return $validator->validated(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Console/MakeActionCommand.php: -------------------------------------------------------------------------------- 1 | resolveStubPath('/stubs/action.stub'); 24 | } 25 | 26 | /** 27 | * Resolve the fully-qualified path to the stub. 28 | */ 29 | protected function resolveStubPath(string $stub): string 30 | { 31 | return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) 32 | ? $customPath 33 | : __DIR__ . $stub; 34 | } 35 | 36 | /** 37 | * Get the default namespace for the class. 38 | */ 39 | protected function getDefaultNamespace($rootNamespace): string 40 | { 41 | return $rootNamespace.'\Actions'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Console/stubs/action.stub: -------------------------------------------------------------------------------- 1 | setAction($action); 17 | 18 | $this->signature = $this->fromActionMethodOrProperty('getCommandSignature', 'commandSignature'); 19 | $this->name = $this->fromActionMethodOrProperty('getCommandName', 'commandName'); 20 | $this->description = $this->fromActionMethodOrProperty('getCommandDescription', 'commandDescription'); 21 | $this->help = $this->fromActionMethodOrProperty('getCommandHelp', 'commandHelp'); 22 | $this->hidden = $this->fromActionMethodOrProperty('isCommandHidden', 'commandHidden', false); 23 | 24 | if (! $this->signature) { 25 | throw new MissingCommandSignatureException($this->action); 26 | } 27 | 28 | parent::__construct(); 29 | } 30 | 31 | public function handle() 32 | { 33 | if ($this->hasMethod('asCommand')) { 34 | return $this->resolveAndCallMethod('asCommand', ['command' => $this]); 35 | } 36 | 37 | if ($this->hasMethod('handle')) { 38 | return $this->resolveAndCallMethod('handle', ['command' => $this]); 39 | } 40 | } 41 | 42 | public function getComponents(): Factory 43 | { 44 | return $this->components; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Decorators/ControllerDecorator.php: -------------------------------------------------------------------------------- 1 | container = Container::getInstance(); 29 | $this->route = $route; 30 | $this->setAction($action); 31 | $this->replaceRouteMethod(); 32 | 33 | if ($this->hasMethod('getControllerMiddleware')) { 34 | $this->middleware = $this->resolveAndCallMethod('getControllerMiddleware'); 35 | } 36 | } 37 | 38 | public function getRoute(): Route 39 | { 40 | return $this->route; 41 | } 42 | 43 | public function getMiddleware(): array 44 | { 45 | return array_map(function ($middleware) { 46 | return [ 47 | 'middleware' => $middleware, 48 | 'options' => [], 49 | ]; 50 | }, $this->middleware); 51 | } 52 | 53 | public function callAction($method, $parameters) 54 | { 55 | return $this->__invoke($method); 56 | } 57 | 58 | public function __invoke(string $method) 59 | { 60 | $this->refreshAction(); 61 | $request = $this->refreshRequest(); 62 | 63 | if ($this->shouldValidateRequest($method)) { 64 | $request->validate(); 65 | } 66 | 67 | $response = $this->run($method); 68 | 69 | if ($this->hasMethod('jsonResponse') && $request->expectsJson()) { 70 | $response = $this->callMethod('jsonResponse', [$response, $request]); 71 | } elseif ($this->hasMethod('htmlResponse') && ! $request->expectsJson()) { 72 | $response = $this->callMethod('htmlResponse', [$response, $request]); 73 | } 74 | 75 | return $response; 76 | } 77 | 78 | protected function refreshAction(): void 79 | { 80 | if ($this->executedAtLeastOne) { 81 | $this->setAction(app(get_class($this->action))); 82 | } 83 | 84 | $this->executedAtLeastOne = true; 85 | } 86 | 87 | protected function refreshRequest(): ActionRequest 88 | { 89 | app()->forgetInstance(ActionRequest::class); 90 | 91 | /** @var ActionRequest $request */ 92 | $request = app(ActionRequest::class); 93 | $request->setAction($this->action); 94 | app()->instance(ActionRequest::class, $request); 95 | 96 | return $request; 97 | } 98 | 99 | protected function replaceRouteMethod(): void 100 | { 101 | if (! isset($this->route->action['uses'])) { 102 | return; 103 | } 104 | 105 | $currentMethod = Str::afterLast($this->route->action['uses'], '@'); 106 | $newMethod = $this->getDefaultRouteMethod(); 107 | 108 | if ($currentMethod !== '__invoke' || $currentMethod === $newMethod) { 109 | return; 110 | } 111 | 112 | $this->route->action['uses'] = (string) Str::of($this->route->action['uses']) 113 | ->beforeLast('@') 114 | ->append('@' . $newMethod); 115 | } 116 | 117 | protected function getDefaultRouteMethod(): string 118 | { 119 | if ($this->hasMethod('asController')) { 120 | return 'asController'; 121 | } 122 | 123 | return $this->hasMethod('handle') ? 'handle' : '__invoke'; 124 | } 125 | 126 | protected function isExplicitMethod(string $method): bool 127 | { 128 | return ! in_array($method, ['asController', 'handle', '__invoke']); 129 | } 130 | 131 | protected function run(string $method) 132 | { 133 | if ($this->hasMethod($method)) { 134 | return $this->resolveFromRouteAndCall($method); 135 | } 136 | } 137 | 138 | protected function shouldValidateRequest(string $method): bool 139 | { 140 | return $this->hasAnyValidationMethod() 141 | && ! $this->isExplicitMethod($method) 142 | && ! $this->hasTrait(WithAttributes::class); 143 | } 144 | 145 | protected function hasAnyValidationMethod(): bool 146 | { 147 | return $this->hasMethod('authorize') 148 | || $this->hasMethod('rules') 149 | || $this->hasMethod('withValidator') 150 | || $this->hasMethod('afterValidator') 151 | || $this->hasMethod('getValidator'); 152 | } 153 | 154 | protected function resolveFromRouteAndCall($method) 155 | { 156 | $this->container = Container::getInstance(); 157 | 158 | $arguments = $this->resolveClassMethodDependencies( 159 | $this->route->parametersWithoutNulls(), 160 | $this->action, 161 | $method 162 | ); 163 | 164 | return $this->action->{$method}(...array_values($arguments)); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Decorators/JobDecorator.php: -------------------------------------------------------------------------------- 1 | actionClass = $action; 40 | $this->setAction(app($action)); 41 | $this->parameters = $parameters; 42 | $this->constructed(); 43 | } 44 | 45 | protected function constructed(): void 46 | { 47 | $this->onConnection($this->fromActionProperty('jobConnection')); 48 | $this->onQueue($this->fromActionProperty('jobQueue')); 49 | $this->setTries($this->fromActionProperty('jobTries')); 50 | $this->setMaxExceptions($this->fromActionProperty('jobMaxExceptions')); 51 | $this->setTimeout($this->fromActionProperty('jobTimeout')); 52 | $this->setDeleteWhenMissingModels($this->fromActionProperty('jobDeleteWhenMissingModels')); 53 | $this->fromActionMethod('configureJob', [$this]); 54 | } 55 | 56 | public function handle() 57 | { 58 | if ($this->hasMethod('asJob')) { 59 | return $this->callMethod('asJob', $this->getPrependedParameters('asJob')); 60 | } 61 | 62 | if ($this->hasMethod('handle')) { 63 | return $this->callMethod('handle', $this->getPrependedParameters('handle')); 64 | } 65 | } 66 | 67 | public function getAction() 68 | { 69 | return $this->action; 70 | } 71 | 72 | public function getParameters(): array 73 | { 74 | return $this->parameters; 75 | } 76 | 77 | public function setTries(?int $tries): self 78 | { 79 | $this->tries = $tries; 80 | 81 | return $this; 82 | } 83 | 84 | public function setMaxExceptions(?int $maxException): self 85 | { 86 | $this->maxExceptions = $maxException; 87 | 88 | return $this; 89 | } 90 | 91 | public function setTimeout(?int $timeout): self 92 | { 93 | $this->timeout = $timeout; 94 | 95 | return $this; 96 | } 97 | 98 | public function setDeleteWhenMissingModels(?bool $deleteWhenMissingModels): self 99 | { 100 | $this->deleteWhenMissingModels = $deleteWhenMissingModels; 101 | 102 | return $this; 103 | } 104 | 105 | public function decorates(string $actionClass): bool 106 | { 107 | return $this->getAction() instanceof $actionClass; 108 | } 109 | 110 | public function backoff() 111 | { 112 | return $this->fromActionMethodOrProperty( 113 | 'getJobBackoff', 114 | 'jobBackoff', 115 | null, 116 | $this->parameters 117 | ); 118 | } 119 | 120 | public function retryUntil() 121 | { 122 | return $this->fromActionMethodOrProperty( 123 | 'getJobRetryUntil', 124 | 'jobRetryUntil', 125 | null, 126 | $this->parameters 127 | ); 128 | } 129 | 130 | public function middleware() 131 | { 132 | return $this->fromActionMethod('getJobMiddleware', $this->parameters, []); 133 | } 134 | 135 | /** 136 | * Laravel will call failed() on a job that fails. This function will call 137 | * the function jobFailed(Throwable $e) on the underlying action if Laravel 138 | * calls the failed() function on the job. 139 | */ 140 | public function failed(Throwable $e): void 141 | { 142 | $this->fromActionMethod('jobFailed', [$e, ...$this->parameters], []); 143 | } 144 | 145 | public function displayName(): string 146 | { 147 | return $this->fromActionMethod( 148 | 'getJobDisplayName', 149 | $this->parameters, 150 | get_class($this->action) 151 | ); 152 | } 153 | 154 | public function tags() 155 | { 156 | return $this->fromActionMethod('getJobTags', $this->parameters, []); 157 | } 158 | 159 | protected function getPrependedParameters(string $method): array 160 | { 161 | $reflectionMethod = new ReflectionMethod($this->action, $method); 162 | $numberOfParameters = $reflectionMethod->getNumberOfParameters(); 163 | 164 | if (! $reflectionMethod->isVariadic() && $numberOfParameters <= count($this->parameters)) { 165 | return $this->parameters; 166 | } 167 | 168 | /** @var ReflectionParameter $firstParameter */ 169 | if (! $firstParameter = Arr::first($reflectionMethod->getParameters())) { 170 | return $this->parameters; 171 | } 172 | 173 | $firstParameterClass = Reflector::getParameterClassName($firstParameter); 174 | 175 | if ($firstParameter->allowsNull() && $firstParameterClass === Batch::class) { 176 | return [$this->batch(), ...$this->parameters]; 177 | } 178 | if (is_subclass_of($firstParameterClass, self::class) || $firstParameterClass === self::class) { 179 | return [$this, ...$this->parameters]; 180 | } 181 | 182 | return $this->parameters; 183 | } 184 | 185 | protected function serializeProperties(): void 186 | { 187 | $this->action = $this->actionClass; 188 | 189 | array_walk($this->parameters, function (&$value) { 190 | $value = $this->getSerializedPropertyValue($value); 191 | }); 192 | } 193 | 194 | protected function unserializeProperties(): void 195 | { 196 | $this->setAction(app($this->actionClass)); 197 | 198 | array_walk($this->parameters, function (&$value) { 199 | $value = $this->getRestoredPropertyValue($value); 200 | }); 201 | } 202 | 203 | public function __serialize(): array 204 | { 205 | $this->serializeProperties(); 206 | 207 | return $this->serializeFromSerializesModels(); 208 | } 209 | 210 | public function __unserialize(array $values): void 211 | { 212 | $this->unserializeFromSerializesModels($values); 213 | $this->unserializeProperties(); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Decorators/ListenerDecorator.php: -------------------------------------------------------------------------------- 1 | setAction($action); 22 | $this->container = new Container; 23 | } 24 | 25 | public function handle(...$arguments) 26 | { 27 | if ($this->hasMethod('asListener')) { 28 | return $this->resolveFromArgumentsAndCall('asListener', $arguments); 29 | } 30 | 31 | if ($this->hasMethod('handle')) { 32 | return $this->resolveFromArgumentsAndCall('handle', $arguments); 33 | } 34 | } 35 | 36 | public function shouldQueue(...$arguments) 37 | { 38 | if ($this->hasMethod('shouldQueue')) { 39 | return $this->resolveFromArgumentsAndCall('shouldQueue', $arguments); 40 | } 41 | 42 | return true; 43 | } 44 | 45 | protected function resolveFromArgumentsAndCall($method, $arguments) 46 | { 47 | $arguments = $this->resolveClassMethodDependencies( 48 | $arguments, 49 | $this->action, 50 | $method 51 | ); 52 | 53 | return $this->action->{$method}(...array_values($arguments)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Decorators/UniqueJobDecorator.php: -------------------------------------------------------------------------------- 1 | uniqueFor = (int) $this->fromActionWithParameters('getJobUniqueFor', 'jobUniqueFor', 0); 16 | 17 | parent::constructed(); 18 | } 19 | 20 | public function uniqueId(): string 21 | { 22 | $uniqueId = $this->fromActionWithParameters('getJobUniqueId', 'jobUniqueId', ''); 23 | $prefix = '.' . get_class($this->action); 24 | $prefix .= $uniqueId ? '.' : ''; 25 | 26 | return $prefix . $uniqueId; 27 | } 28 | 29 | public function uniqueVia() 30 | { 31 | return $this->fromActionMethod('getJobUniqueVia', $this->parameters, function () { 32 | return Container::getInstance()->make(Cache::class); 33 | }); 34 | } 35 | 36 | protected function fromActionWithParameters(string $method, string $property, $default = null) 37 | { 38 | return $this->fromActionMethodOrProperty($method, $property, $default, $this->parameters); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/DesignPatterns/CommandDesignPattern.php: -------------------------------------------------------------------------------- 1 | matches(Application::class, 'resolve') 21 | || $frame->matches(Schedule::class, 'command'); 22 | } 23 | 24 | public function decorate($instance, BacktraceFrame $frame) 25 | { 26 | return app(CommandDecorator::class, ['action' => $instance]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DesignPatterns/ControllerDesignPattern.php: -------------------------------------------------------------------------------- 1 | matches(Route::class, 'getController'); 20 | } 21 | 22 | public function decorate($instance, BacktraceFrame $frame) 23 | { 24 | return app(ControllerDecorator::class, [ 25 | 'action' => $instance, 26 | 'route' => $frame->getObject(), 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DesignPatterns/DesignPattern.php: -------------------------------------------------------------------------------- 1 | matches(Dispatcher::class, 'dispatch') 21 | || $frame->matches(Dispatcher::class, 'handlerWantsToBeQueued') 22 | || $frame->matches(CallQueuedListener::class, 'handle') 23 | || $frame->matches(CallQueuedListener::class, 'failed'); 24 | } 25 | 26 | public function decorate($instance, BacktraceFrame $frame) 27 | { 28 | return app(ListenerDecorator::class, ['action' => $instance]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exceptions/MissingCommandSignatureException.php: -------------------------------------------------------------------------------- 1 |