├── .scrutinizer.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── flora.php ├── declaration.php ├── laravel-logo.png ├── phpstan.neon.rector.dist ├── rector.php └── src ├── Actions ├── Action.php ├── ActionTerminatedException.php ├── Artisan.php ├── Callback.php ├── Job.php ├── Notification.php ├── Process.php └── Script.php ├── AssetPublishException.php ├── AssetsVersion.php ├── Chain.php ├── ChainVault.php ├── Console ├── Assets.php ├── Commands │ ├── FloraCommand.php │ ├── InstallCommand.php │ ├── PackageInstruction.php │ ├── SetupCommand.php │ └── UpdateCommand.php └── StopSetupException.php ├── Contracts ├── Chain.php └── ChainVault.php ├── Discovers ├── HorizonDiscover.php ├── IdeHelperDiscover.php ├── Instruction.php ├── PackageDiscover.php ├── PackageDiscoverException.php ├── TrailDiscover.php ├── TypeScriptTransformerDiscover.php └── VaporUiDiscover.php ├── Enums ├── Environment.php └── FloraType.php ├── FloraServiceProvider.php ├── Run.php ├── RunInternal.php ├── SetupInstructions.php ├── UndefinedInstructionException.php ├── UndefinedScriptException.php ├── helpers.php └── setup.php /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [tests/*] 3 | 4 | checks: 5 | php: 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | 20 | tools: 21 | external_code_coverage: 22 | timeout: 600 23 | runs: 3 24 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `laravel-flora` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | ## 1.2.0 - 2024-04-13 8 | 9 | - Laravel 11 support 10 | 11 | ## 1.1.0 - 2023-08-24 12 | 13 | - `npm install` command in **build** script switched to `npm ci` 14 | - [spatie/laravel-typescript-transformer](https://spatie.be/docs/typescript-transformer/v2/introduction) support. Executes `php artisan typescript:transform` command on every environment. https://github.com/qruto/laravel-flora/commit/912205bbb5a096b23361bf8e8cca1410af944c40 15 | - [based/momentum-trail](https://github.com/qruto/laravel-flora/commit/fc3a32c457e0be8c7309ecce767fa333cde2034b) support. Executes `php artisan trail:generate` on every environment. https://github.com/qruto/laravel-flora/commit/fc3a32c457e0be8c7309ecce767fa333cde2034b 16 | 17 | ## 1.0.0 - 2023-03-07 18 | 19 | New generation release 🎉 20 | Check out [upgrade guide](https://github.com/qruto/laravel-flora/blob/main/UPGRADING.md) to switch from outdated version. 21 | 22 | ⚒️ New `Process` layer for execution 23 | 24 | 🎨 Sweet Laravel output components 25 | 26 | 🚥 Signals handling support 27 | 28 | 📦 Auto instructions for popular packages 29 | 30 | ## Previous generation changelog 31 | 32 | You can find outdated changelog in [previous repository](https://github.com/mad-web/laravel-initializer/blob/master/CHANGELOG.md) 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Qruto , Slava Razum 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 |

2 | 3 | 8 | 13 | Laravel Flora Logo 18 | 19 |

20 | 21 |

A convenient way to automate setup of your application.

22 | 23 |

24 | 25 | 30 | 35 | Laravel Flora Code Example 40 | 41 |

42 | 43 |

44 | Build Status 45 | Styles check 46 | Types check 47 | Refactor code 48 | Total Downloads 49 | Latest Stable Version 50 |

51 |

52 | Laravel Flora Demo 53 |

54 | 55 | ## Goal 56 | 57 | The main goal of _Flora_ is define and automate the setup process of Laravel application. 58 | All necessary actions to make the application ready to work in one place. 59 | 60 | Packages discovering, assets building and publishing, running database migrations, caching etc... 61 | 62 | > 🧠🚀 Put the knowledge of setup instructions at the application level. 63 | 64 | ## Introduction 65 | 66 | Revival of [Laravel Initializer](https://laravel-news.com/automate-app-setup-with-laravel-initializer). Rethinked, improved, prettified, renamed. 67 | 68 | _Flora_ allows you to bring Laravel application to live by one command. 69 | Use default or define custom chain of actions required to **install** or **update** an application. 70 | 71 | Run `install` when you fetch a fresh application to prepare it to launch on new environment. 72 | 73 | - after `git clone` 74 | 75 | Run `update` on every dependency or source code change. 76 | 77 | - after `composer install|update` 78 | - after `git pull|checkout|megre|...` 79 | - in deploy script 80 | - in CI/CD pipeline 81 | 82 | it will take care of the rest of the work. 83 | 84 | ## Support 85 | 86 | Since of February 24, unfortunately I haven't any commercial work, permanent living place or the ability to plan anything for the long term. However, I have a greater desire to continue creating useful solutions for people around the world. It makes me feel better these days. 87 | 88 | [![support me](https://raw.githubusercontent.com/slavarazum/slavarazum/main/support-banner.png)](https://github.com/sponsors/qruto) 89 | 90 | [GitHub Sponsorships profile](https://github.com/sponsors/qruto) is ready! There you can find current work, future plans, goals and dreams... 91 | Your stars make me happier each day ❤️ ⭐ Sponsorship will enable us to live more peacefully and continue to work on useful solutions for you. 92 | 93 | I would be very grateful for mentions or just a sincere "thank you". 94 | 95 | 💳 [Sponsoring directly to savings jar](https://send.monobank.ua/jar/3eG4Vafvzq) with card or Apple Pay/Google Pay. 96 | 97 | ## Installation 98 | 99 | Via Composer: 100 | 101 | ``` bash 102 | composer require qruto/laravel-flora 103 | ``` 104 | 105 | ## Usage 106 | 107 | Replace ~~**installation**~~ section in readme file with: 108 | 109 | ```bash 110 | php artisan install 111 | ``` 112 | 113 | Refresh application state by: 114 | 115 | ```bash 116 | php artisan update 117 | ``` 118 | 119 | > ℹ️ Instruction depends on current **environment**. Package has predefined actions suitable for most cases. 120 | 121 | See detailed output in verbosity mode: 122 | 123 | ```bash 124 | php artisan app:update -v 125 | ``` 126 | 127 | You can automate the update process by adding `@php artisan update` command to your application 128 | `composer.json` script `post-autoload-dump` section and remove 129 | default `vendor:publish` command from `post-update-cmd` section. 130 | `update` command will take care of assets publishing for you. 131 | 132 | Setup it with: 133 | 134 | ```bash 135 | php artisan flora:setup --script 136 | ``` 137 | 138 | `composer.json` changes: 139 | 140 | ```diff 141 | "post-autoload-dump": [ 142 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 143 | - "@php artisan package:discover --ansi" 144 | + "@php artisan update" 145 | ], 146 | - "post-update-cmd": [ 147 | - "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 148 | - ], 149 | ``` 150 | 151 | ### Register Task Scheduler 152 | 153 | Conditions: 154 | - any scheduler task registered 155 | - installation process performed 156 | - application in production environment 157 | 158 | then you will be prompted for the addition of a cron entry to [run the task scheduler every minute](https://laravel.com/docs/master/scheduling#running-the-scheduler). 159 | 160 | ![Task Scheduler](https://github.com/qruto/laravel-flora/raw/HEAD/art/task-scheduling.png) 161 | 162 | ## Configuration 163 | 164 | To customize instructions for each environment, you need to publish setup files. 165 | 166 | ```bash 167 | php artisan flora:setup 168 | ``` 169 | 170 | This command will create `routes/setup.php` file with predefined instructions for `local` and `production` environments. 171 | 172 | ```php 173 | use Qruto\Flora\Run; 174 | 175 | App::install('local', fn (Run $run) => $run 176 | ->command('key:generate') 177 | ->command('migrate') 178 | ->command('storage:link') 179 | ->script('build') 180 | ); 181 | 182 | App::install('production', fn (Run $run) => $run 183 | ->command('key:generate', ['--force' => true]) 184 | ->command('migrate', ['--force' => true]) 185 | ->command('storage:link') 186 | ->script('cache') 187 | ->script('build') 188 | ); 189 | 190 | App::update('local', fn (Run $run) => $run 191 | ->command('migrate') 192 | ->command('cache:clear') 193 | ->script('build') 194 | ); 195 | 196 | App::update('production', fn (Run $run) => $run 197 | ->script('cache') 198 | ->command('migrate', ['--force' => true]) 199 | ->command('cache:clear') 200 | ->command('queue:restart') 201 | ->script('build') 202 | ); 203 | ``` 204 | 205 | Feel free to change it any way you need or add specific environment like `staging`. 206 | 207 |
208 | 209 | `build` and `cache` script details 210 | 211 | `build` script contains assets building commands: 212 | 213 | ```bash 214 | npm install 215 | npm run build 216 | ``` 217 | 218 | `cache` script provides general application caching: 219 | 220 | ```bash 221 | php artisan route:cache 222 | php artisan config:cache 223 | php artisan event:cache 224 | ``` 225 |
226 | 227 | In addition, it will create `config/flora.php` for configuration assets publishing. 228 | 229 | ```php 230 | return [ 231 | /* 232 | |-------------------------------------------------------------------------- 233 | | Force Assets Publish 234 | |-------------------------------------------------------------------------- 235 | | 236 | | Force publish assets on every installation or update. By default, assets 237 | | will always be force published, which would completely automate the 238 | | setup. Switch it to false if you want to manually publish assets. 239 | | For example if you prefer to commit them. 240 | */ 241 | 'force_publish' => true, 242 | 243 | /* 244 | |-------------------------------------------------------------------------- 245 | | Publishable Assets 246 | |-------------------------------------------------------------------------- 247 | | 248 | | List of assets that will be published during installation or update. 249 | | Most of required assets detects on the way. If you need specific 250 | | tag or provider, feel free to add it to the array. 251 | */ 252 | 'assets' => [ 253 | 'laravel-assets', 254 | ], 255 | ]; 256 | ``` 257 | 258 | If you need to customize just assets publishing, you can publish only configuration file: 259 | 260 | ```bash 261 | php artisan vendor:publish --tag=flora-config 262 | ``` 263 | 264 | ### Side Packages Support 265 | 266 | _Flora_ automatically detects several packages for performing necessary actions on install or update. 267 | For example: publish Vapor UI assets, generate IDE helper files, terminate Horizon workers etc. 268 | 269 | Supported: 270 | 271 | - [Laravel Vapor Ui](https://github.com/laravel/vapor-ui) 272 | - [Laravel Horizon](https://github.com/laravel/horizon) 273 | - [IDE Helper for Laravel](https://github.com/barryvdh/laravel-ide-helper) 274 | 275 | Soon: 276 | 277 | - [ ] [Laravel Octane](https://laravel.com/docs/10.x/octane#reloading-the-workers) 278 | - [ ] [Laravel Nova](https://nova.laravel.com/docs/4.0/installation.html#updating-nova-s-assets) 279 | - [ ] [Laravel Passport](https://laravel.com/docs/10.x/passport#deploying-passport) 280 | 281 | ### Custom Scripts 282 | 283 | Override or define custom script in service provider's `boot` method: 284 | 285 | ```php 286 | Run::newScript('cache', fn (Run $run) => $run 287 | ->command('route:cache') 288 | ->command('config:cache') 289 | ->command('event:cache') 290 | ->command('view:cache') 291 | ); 292 | ``` 293 | 294 | ### Available Actions 295 | 296 | ```php 297 | $run 298 | ->command('command') // Run artisan command 299 | ->script('build') // Perform custom script 300 | ->exec('process') // Execute external process 301 | ->job(new JobClass) // Dispatch job 302 | ->call(fn () => makeSomething()) // Call callable function 303 | ->notify('Done!') // Send notification 304 | ``` 305 | 306 | ## Upgrading 307 | 308 | Please see [UPGRADING](UPGRADING.md) for details. 309 | 310 | ## Changelog 311 | 312 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 313 | 314 | ## Testing 315 | 316 | ```bash 317 | composer test 318 | ``` 319 | 320 | ## Contributing 321 | 322 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) and [CONDUCT](.github/CODE_OF_CONDUCT.md) for details. 323 | 324 | ## Security 325 | 326 | If you discover any security related issues, please email bro@qruto.to instead of using the issue tracker. 327 | 328 | ## Credits 329 | 330 | Thanks [Nuno Maduro](https://github.com/nunomaduro) for [laravel-desktop-notifier](https://github.com/nunomaduro/laravel-desktop-notifier) package which brings desktop notifications to Laravel. 331 | 332 | - [Qruto](https://github.com/qruto) 333 | - [All Contributors](../../contributors) 334 | 335 | ## License 336 | 337 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 338 | 339 | [link-author]: https://github.com/qruto 340 | [link-contributors]: ../../contributors 341 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qruto/laravel-flora", 3 | "description": "Install and update Laravel application with single command", 4 | "keywords": [ 5 | "qruto", 6 | "laravel-flora", 7 | "install", 8 | "update", 9 | "deploy", 10 | "init", 11 | "initialize", 12 | "setup", 13 | "exec", 14 | "command", 15 | "process", 16 | "job", 17 | "tasks" 18 | ], 19 | "homepage": "https://github.com/qruto/laravel-flora", 20 | "support": { 21 | "issues": "https://github.com/qruto/laravel-wave/issues", 22 | "source": "https://github.com/qruto/laravel-wave" 23 | }, 24 | "license": "MIT", 25 | "authors": [ 26 | { 27 | "name": "Slava Razum", 28 | "email": "razum@qruto.to", 29 | "homepage": "https://github.com/slavarazum", 30 | "role": "Developer" 31 | } 32 | ], 33 | "require": { 34 | "php": "^8.1", 35 | "illuminate/bus": "^10.0|^11.0", 36 | "illuminate/config": "^10.0|^11.0", 37 | "illuminate/console": "^10.0|^11.0", 38 | "illuminate/container": "^10.0|^11.0", 39 | "illuminate/support": "^10.0|^11.0", 40 | "nunomaduro/laravel-desktop-notifier": "^2.8", 41 | "spatie/laravel-package-tools": "^1.16" 42 | }, 43 | "require-dev": { 44 | "driftingly/rector-laravel": "^1.1", 45 | "larastan/larastan": "^2.9", 46 | "laravel/pint": "^v1.15", 47 | "mockery/mockery": "^1.6", 48 | "orchestra/canvas": "^8.12|^9.0", 49 | "orchestra/testbench": "^8.0|^9.0", 50 | "pestphp/pest": "^2.0", 51 | "pestphp/pest-plugin-laravel": "^2.0", 52 | "phpstan/extension-installer": "^1.3", 53 | "phpstan/phpstan-deprecation-rules": "^1.1", 54 | "phpstan/phpstan-phpunit": "^1.3", 55 | "phpunit/phpunit": "^9.6|^10.0", 56 | "rector/rector": "^1.0", 57 | "spatie/laravel-ray": "^1.36" 58 | }, 59 | "autoload": { 60 | "psr-4": { 61 | "Qruto\\Flora\\": "src" 62 | }, 63 | "files": [ 64 | "src/helpers.php" 65 | ] 66 | }, 67 | "autoload-dev": { 68 | "psr-4": { 69 | "Qruto\\Flora\\Tests\\": "tests" 70 | } 71 | }, 72 | "scripts": { 73 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 74 | "lint": "pint -v", 75 | "refactor": "rector --debug", 76 | "test-coverage": "pest --coverage --colors=always", 77 | "test:refactor": "rector --dry-run", 78 | "test:types": "phpstan analyse --ansi --memory-limit=-1", 79 | "test:unit": "pest --colors=always", 80 | "test:lint": "pint --test -v", 81 | "test": [ 82 | "@test:lint", 83 | "@test:refactor", 84 | "@test:types", 85 | "@test:unit" 86 | ], 87 | "fix": [ 88 | "@refactor", 89 | "@lint" 90 | ] 91 | }, 92 | "config": { 93 | "sort-packages": true, 94 | "allow-plugins": { 95 | "pestphp/pest-plugin": true, 96 | "phpstan/extension-installer": true 97 | } 98 | }, 99 | "extra": { 100 | "laravel": { 101 | "providers": [ 102 | "Qruto\\Flora\\FloraServiceProvider" 103 | ] 104 | } 105 | }, 106 | "minimum-stability": "dev", 107 | "prefer-stable": true 108 | } 109 | -------------------------------------------------------------------------------- /config/flora.php: -------------------------------------------------------------------------------- 1 | true, 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Publishable Assets 19 | |-------------------------------------------------------------------------- 20 | | 21 | | List of assets that will be published during installation or update. 22 | | Most of required assets detects on the way. If you need specific 23 | | tag or provider, feel free to add it to the array. 24 | */ 25 | 'assets' => [ 26 | 'laravel-assets', 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /declaration.php: -------------------------------------------------------------------------------- 1 | withPaths([ 12 | __DIR__.'/src', 13 | ]) 14 | ->withRules([InlineConstructorDefaultToPropertyRector::class]) 15 | ->withPreparedSets( 16 | codeQuality: true, 17 | deadCode: true, 18 | earlyReturn: true, 19 | ) 20 | ->withSets([ 21 | LaravelSetList::LARAVEL_110, 22 | ]) 23 | ->withPhpVersion(PhpVersion::PHP_81) 24 | ->withBootstrapFiles([__DIR__.'/vendor/larastan/larastan/bootstrap.php']) 25 | ->withPHPStanConfigs([__DIR__.'/phpstan.neon.rector.dist']); 26 | -------------------------------------------------------------------------------- /src/Actions/Action.php: -------------------------------------------------------------------------------- 1 | successful = $this->run(); 42 | } catch (ActionTerminatedException $e) { 43 | $this->terminated = true; 44 | 45 | return $this->successful = true; 46 | } catch (StopSetupException $e) { 47 | throw $e; 48 | } catch (Exception $e) { 49 | $this->exception = $e; 50 | 51 | return $this->successful = false; 52 | } 53 | }; 54 | 55 | if ($this->silent) { 56 | return $callback(); 57 | } 58 | 59 | $outputComponents->task($this->title($labelWidth), $callback); 60 | 61 | if ($this->terminated) { 62 | $this->output->write("\x1B[1A"); 63 | $this->output->write("\x1B[2K"); 64 | } 65 | 66 | return $this->failed(); 67 | } 68 | 69 | /** Determines whether the action has failed */ 70 | public function failed(): bool 71 | { 72 | return ! $this->successful; 73 | } 74 | 75 | /** Determines whether the action has been terminated */ 76 | public function terminated(): bool 77 | { 78 | return $this->terminated; 79 | } 80 | 81 | /** Get exception thrown during performing */ 82 | public function getException(): ?Throwable 83 | { 84 | return $this->exception; 85 | } 86 | 87 | /** Set the output interface */ 88 | public function withOutput(OutputInterface $output): static 89 | { 90 | $this->output = $output; 91 | 92 | return $this; 93 | } 94 | 95 | /** Returns spaces string required to format label width */ 96 | private function spaces(string $title, int $width): string 97 | { 98 | if ($width === 0) { 99 | return ''; 100 | } 101 | 102 | return str_repeat(' ', $width - strlen($title)); 103 | } 104 | 105 | /** Returns formatted action title */ 106 | public function title(int $width = 0): string 107 | { 108 | $name = $this->name(); 109 | $title = static::$label; 110 | $description = $this->description(); 111 | 112 | $spaces = $this->spaces($title, $width); 113 | $title = "color};options=bold>$title$spaces $name"; 114 | 115 | if ($description !== '' && $description !== '0') { 116 | $title .= " $description"; 117 | } 118 | 119 | return $title; 120 | } 121 | 122 | /** Returns action name */ 123 | abstract public function name(): string; 124 | 125 | /** Returns action description */ 126 | protected function description(): string 127 | { 128 | return ''; 129 | } 130 | 131 | /** Run action specific needs */ 132 | abstract public function run(): bool; 133 | 134 | /** Determines whether action is silent */ 135 | public function isSilent(): bool 136 | { 137 | return $this->silent; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Actions/ActionTerminatedException.php: -------------------------------------------------------------------------------- 1 | name(), $signal)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Actions/Artisan.php: -------------------------------------------------------------------------------- 1 | command; 26 | } 27 | 28 | /** Get artisan command description */ 29 | protected function description(): string 30 | { 31 | if (! $this->output->isVerbose()) { 32 | return ''; 33 | } 34 | 35 | return $this->application->find($this->command)->getDescription(); 36 | } 37 | 38 | /** Get artisan command parameters */ 39 | public function getParameters(): array 40 | { 41 | return $this->parameters; 42 | } 43 | 44 | /** Call artisan command in no interaction mode */ 45 | public function run(): bool 46 | { 47 | return $this->application->call($this->command, $this->parameters + ['--no-interaction' => true]) === 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Actions/Callback.php: -------------------------------------------------------------------------------- 1 | name) { 32 | $name = $this->name; 33 | } else { 34 | is_callable($this->callback, callable_name: $name); 35 | } 36 | 37 | return $name; 38 | } 39 | 40 | /** Run callback with service container */ 41 | public function run(): bool 42 | { 43 | $this->container->call($this->callback, $this->parameters); 44 | 45 | return true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Actions/Job.php: -------------------------------------------------------------------------------- 1 | job) ? $this->job : $this->job::class; 27 | } 28 | 29 | /** Dispatch job immediately */ 30 | public function run(): bool 31 | { 32 | $dispatcher = Container::getInstance()->make(Dispatcher::class); 33 | 34 | $job = is_string($this->job) ? Container::getInstance()->make($this->job) : $this->job; 35 | 36 | $dispatcher->dispatchNow($job); 37 | 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Actions/Notification.php: -------------------------------------------------------------------------------- 1 | string; 29 | } 30 | 31 | /** Perform desktop notification */ 32 | public function run(): bool 33 | { 34 | $notifier = $this->container[Notifier::class]; 35 | 36 | $notification = $this->container[NotificationAlias::class] 37 | ->setTitle($this->string) 38 | ->setBody($this->body); 39 | 40 | $notification->setIcon($this->icon === null || $this->icon === '' || $this->icon === '0' ? __DIR__.'/../../laravel-logo.png' : $this->icon); 41 | 42 | return $notifier->send($notification); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Actions/Process.php: -------------------------------------------------------------------------------- 1 | parameters); 27 | 28 | return $this->command.($argumentsString !== '' ? ' '.$argumentsString : ''); 29 | } 30 | 31 | /** Handle custom process */ 32 | public function run(): bool 33 | { 34 | $result = $this->runProcess(); 35 | 36 | if ($result->successful()) { 37 | return true; 38 | } 39 | 40 | throw new RuntimeException(trim($result->errorOutput()), $result->exitCode()); 41 | } 42 | 43 | /** Run custom process */ 44 | private function runProcess(): ProcessResult 45 | { 46 | if ($this->parameters === []) { 47 | return ProcessFacade::forever()->run($this->command); 48 | } 49 | 50 | return ProcessFacade::forever()->run(array_merge([$this->command], $this->parameters)); 51 | } 52 | 53 | /** Get custom process command name */ 54 | public function getCommand(): string 55 | { 56 | return $this->command; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Actions/Script.php: -------------------------------------------------------------------------------- 1 | container->call($this->callback, ['run' => $this->run, ...$this->arguments]); 31 | } 32 | 33 | /** Get name of custom script */ 34 | public function name(): string 35 | { 36 | return $this->name; 37 | } 38 | 39 | /** Handle custom script with chain of actions */ 40 | public function __invoke(Factory $outputComponents, int $labelWidth = 0): bool 41 | { 42 | $callback = fn (): bool => $this->successful = $this->run($labelWidth); 43 | 44 | $title = $this->title($labelWidth); 45 | 46 | if ($this->output->isVerbose()) { 47 | $this->output->write(" $title "); 48 | $this->writeDotsLine($labelWidth); 49 | 50 | $callback(); 51 | } else { 52 | $outputComponents->task($title, $callback); 53 | } 54 | 55 | if ($this->run->internal->terminated()) { 56 | clearOutputLineAbove($this->output); 57 | } 58 | 59 | if ($this->run->internal->doneWithFailures() && $this->run->internal->exceptions() !== []) { 60 | $this->exception = $this->run->internal->exceptions()[0]['e']; 61 | } 62 | 63 | return $this->failed(); 64 | } 65 | 66 | /** Run custom script with chain of actions */ 67 | public function run(int $labelWidth = 0): bool 68 | { 69 | $this->run->internal->breakOnTerminate()->start($labelWidth); 70 | 71 | if ($this->output->isVerbose()) { 72 | $this->writeDotsLine(); 73 | } 74 | 75 | return ! $this->run->internal->doneWithFailures(); 76 | } 77 | 78 | /** Write dots divider line to the output */ 79 | private function writeDotsLine(int $offset = 0): void 80 | { 81 | $width = min(terminal()->width(), 150); 82 | $dots = max($width - $offset - 11, 0); 83 | 84 | if ($offset === 0) { 85 | $this->output->write(' '); 86 | $dots += 7; 87 | } 88 | 89 | $this->output->writeLn(str_repeat('.', $dots), false); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/AssetPublishException.php: -------------------------------------------------------------------------------- 1 | latestHash = $this->cache->get('assets_hash'); 14 | } 15 | 16 | public function outdated(): bool 17 | { 18 | $currentHash = $this->currentHash(); 19 | $latestHash = $this->latestHash(); 20 | 21 | if ($latestHash === null) { 22 | return true; 23 | } 24 | 25 | if ($currentHash === null) { 26 | return true; 27 | } 28 | 29 | return $latestHash !== $currentHash; 30 | } 31 | 32 | public function stampUpdate(): void 33 | { 34 | $this->cache->put('assets_hash', $this->currentHash()); 35 | } 36 | 37 | protected function currentHash() 38 | { 39 | $composerLockPath = base_path('composer.lock'); 40 | 41 | return file_exists($composerLockPath) 42 | ? json_decode(file_get_contents($composerLockPath), true, 512, JSON_THROW_ON_ERROR)['content-hash'] 43 | : null; 44 | } 45 | 46 | protected function latestHash(): ?string 47 | { 48 | return $this->latestHash; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Chain.php: -------------------------------------------------------------------------------- 1 | collection[$environment] = $callback; 14 | } 15 | 16 | public function get(string $environment): callable 17 | { 18 | if (! array_key_exists($environment, $this->collection)) { 19 | throw new UndefinedInstructionException($environment); 20 | } 21 | 22 | return $this->collection[$environment]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ChainVault.php: -------------------------------------------------------------------------------- 1 | chains[FloraType::Install->value] = $install; 16 | $this->chains[FloraType::Update->value] = $update; 17 | } 18 | 19 | public function get(FloraType $type): Chain 20 | { 21 | return $this->chains[$type->value]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Console/Assets.php: -------------------------------------------------------------------------------- 1 | Assets publishing'; 19 | 20 | public function __construct(protected Dispatcher $events, protected Kernel $artisan, protected Repository $config) 21 | { 22 | } 23 | 24 | public function publish(Factory $components, bool $verbose = false): bool 25 | { 26 | $assets = $this->config['flora.assets']; 27 | 28 | foreach (resolve('flora.packages') as $package) { 29 | if (! $package->exists()) { 30 | continue; 31 | } 32 | if (! ($tag = $package->instruction()->assetsTag)) { 33 | continue; 34 | } 35 | $assets[] = $tag; 36 | } 37 | 38 | if ($assets === []) { 39 | return true; 40 | } 41 | 42 | try { 43 | if ($verbose) { 44 | $this->runVerbose($assets, $components); 45 | } else { 46 | $this->run($assets, $components); 47 | } 48 | } catch (AssetPublishException) { 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | private function run(array $assets, Factory $components): void 56 | { 57 | $components->task(self::$title, $this->makePublishCallback($assets)); 58 | } 59 | 60 | private function runVerbose(array $assets, Factory $components): void 61 | { 62 | $this->events->listen(static function (VendorTagPublished $event) use ($components): void { 63 | foreach ($event->paths as $from => $to) { 64 | $assetType = null; 65 | 66 | if (is_file($from)) { 67 | $assetType = 'file'; 68 | } elseif (is_dir($from)) { 69 | $assetType = 'directory'; 70 | } 71 | 72 | $assetType ? $components->task(sprintf( 73 | 'Copying %s [%s] to [%s]', 74 | $assetType, 75 | realpath($from), 76 | realpath($to), 77 | )) : $components->error("Can't locate path: <{$from}>"); 78 | } 79 | }); 80 | 81 | $components->twoColumnDetail( 82 | sprintf('%s %s', self::$title, $this->assetsString($assets)) 83 | ); 84 | 85 | $this->makePublishCallback($assets)(); 86 | } 87 | 88 | private function assetsString(array $assets): string 89 | { 90 | $assetsString = ''; 91 | 92 | foreach ($assets as $key => $value) { 93 | if (is_string($key)) { 94 | $assetsString .= $key.': '.(is_array($value) ? implode(', ', $value) : $value); 95 | } else { 96 | $assetsString .= $value; 97 | } 98 | 99 | $assetsString .= ', '; 100 | } 101 | 102 | return rtrim($assetsString, ', '); 103 | } 104 | 105 | private function makePublishCallback(array $assets): Closure 106 | { 107 | $tags = []; 108 | $publishCallbacks = []; 109 | $forced = $this->config['flora.force_publish']; 110 | 111 | foreach ($assets as $key => $value) { 112 | $parameters = ['--provider' => '', '--tag' => []]; 113 | 114 | if (is_string($key)) { 115 | $parameters['--provider'] = $key; 116 | $parameters['--tag'] = is_string($value) ? [$value] : $value; 117 | } elseif (class_exists($value)) { 118 | $parameters['--provider'] = $value; 119 | unset($parameters['--tag']); 120 | } else { 121 | $tags[] = $value; 122 | } 123 | 124 | if (! ($parameters['--provider'] !== '' && $parameters['--provider'] !== '0')) { 125 | continue; 126 | } 127 | 128 | $publishCallbacks[] = fn (): bool => $this->artisan->call('vendor:publish', $parameters + ['--force' => $forced]) === 0; 129 | } 130 | 131 | if ($tags !== []) { 132 | $publishCallbacks[] = fn (): bool => $this->artisan->call('vendor:publish', ['--tag' => $tags, '--force' => $forced]) === 0; 133 | } 134 | 135 | return static fn (): Collection => collect($publishCallbacks) 136 | ->map(static fn (callable $callback): bool => $callback()) 137 | ->each(static fn ($value): bool => throw_if(! $value, AssetPublishException::class)); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Console/Commands/FloraCommand.php: -------------------------------------------------------------------------------- 1 | make(Run::class, [ 48 | 'application' => $this->getApplication(), 49 | 'output' => $this->getOutput(), 50 | ]); 51 | 52 | $this->trap([SIGTERM, SIGINT], function ($signal) use ($run) { 53 | if ($this->components->confirm('Sure you want to stop')) { 54 | $this->components->warn(ucfirst($this->title()).' aborted without completion'); 55 | 56 | throw new StopSetupException(); 57 | } 58 | 59 | $run->internal->rerunLatestAction(); 60 | 61 | throw new ActionTerminatedException($run->internal->getLatestAction(), $signal); 62 | }); 63 | 64 | try { 65 | return $this->perform($vault, $container, $run, $assetsVersion, $exceptionHandler, $schedule, $instructions); 66 | } catch (StopSetupException) { 67 | clearOutputLineAbove($this->output); 68 | 69 | return self::FAILURE; 70 | } 71 | } 72 | 73 | /** 74 | * Returns vault of instructions for current command type. 75 | */ 76 | protected function getFlora(ChainVault $vault): Chain 77 | { 78 | return $vault->get($this->type); 79 | } 80 | 81 | /** 82 | * Returns command action title. 83 | */ 84 | abstract protected function title(): string; 85 | 86 | /** 87 | * Ask user to show errors. 88 | */ 89 | private function askToShowErrors(array $exceptions, ExceptionHandler $exceptionHandler): void 90 | { 91 | if ($exceptions === []) { 92 | return; 93 | } 94 | 95 | if (! $this->components->confirm('Show errors?')) { 96 | return; 97 | } 98 | 99 | foreach ($exceptions as $exception) { 100 | $this->components->twoColumnDetail($exception['title'], 'FAIL'); 101 | 102 | $exceptionHandler->renderForConsole($this->getOutput(), $exception['e']); 103 | $exceptionHandler->report($exception['e']); 104 | 105 | $this->output->newLine(); 106 | $this->output->newLine(); 107 | } 108 | } 109 | 110 | private function discoverPackages(): bool 111 | { 112 | if ($this->output->isVerbose()) { 113 | return $this->call('package:discover') === 0; 114 | } 115 | 116 | try { 117 | /** @throws PackageDiscoverException */ 118 | $this->components->task( 119 | 'Packages discovery', 120 | function () { 121 | if ($this->callSilent('package:discover') !== 0) { 122 | throw new PackageDiscoverException(); 123 | } 124 | } 125 | ); 126 | 127 | return true; 128 | } catch (PackageDiscoverException) { 129 | return false; 130 | } 131 | } 132 | 133 | private function publishAssets(AssetsVersion $assetsVersion): bool 134 | { 135 | $success = true; 136 | 137 | if ($assetsVersion->outdated()) { 138 | $success = $this->laravel[Assets::class]->publish($this->components, $this->output->isVerbose()); 139 | } else { 140 | $this->components->twoColumnDetail('No assets for publishing'); 141 | } 142 | 143 | if ($this->output->isVerbose()) { 144 | $this->output->newLine(); 145 | } 146 | 147 | return $success; 148 | } 149 | 150 | private function registerScheduler(string $env, Schedule $schedule): void 151 | { 152 | if (any( 153 | fn (): bool => $this->type !== FloraType::Install, 154 | fn (): bool => Environment::Production->value !== $env, 155 | fn (): bool => $schedule->events() === [], 156 | )) { 157 | return; 158 | } 159 | 160 | $task = sprintf('* * * * * cd %s && php artisan schedule:run >> /dev/null 2>&1', base_path()); 161 | 162 | $result = Process::run('crontab -l'); 163 | 164 | if (str_contains($result->output(), $task)) { 165 | $this->components->warn('Cron entry for task scheduling already exists'); 166 | 167 | return; 168 | } 169 | 170 | if (! $this->components->confirm('Add a cron entry for task scheduling?')) { 171 | return; 172 | } 173 | 174 | Process::run(sprintf( 175 | '(crontab -l 2>/dev/null; echo "%s") | crontab -', 176 | $task 177 | )); 178 | 179 | $this->components->info("Entry was added [{$task}]"); 180 | } 181 | 182 | private function perform( 183 | ChainVault $vault, 184 | Container $container, 185 | Run $run, 186 | AssetsVersion $assetsVersion, 187 | ExceptionHandler $exceptionHandler, 188 | Schedule $schedule, 189 | SetupInstructions $instructions, 190 | ): int { 191 | $instructions->load(); 192 | 193 | $autoInstruction = ! $instructions->customExists(); 194 | 195 | $flora = $this->getFlora($vault); 196 | 197 | $env = $this->getLaravel()->environment(); 198 | 199 | try { 200 | $container->call($flora->get($env), ['run' => $run]); 201 | } catch (UndefinedScriptException|UndefinedInstructionException $e) { 202 | $this->components->error($e->getMessage()); 203 | 204 | return self::FAILURE; 205 | } 206 | 207 | if ($autoInstruction) { 208 | $this->instructPackages($this->type, $env, $run); 209 | } 210 | 211 | $this->components->alert(sprintf('Application %s', $this->title())); 212 | 213 | $this->output->newLine(); 214 | 215 | $packagesDiscovered = $this->discoverPackages(); 216 | 217 | if ($this->output->isVerbose()) { 218 | $this->components->info('Running actions'); 219 | } else { 220 | $this->output->newLine(); 221 | } 222 | 223 | $run->internal->start(); 224 | 225 | $this->output->newLine(); 226 | 227 | $assetsPublished = $this->publishAssets($assetsVersion); 228 | 229 | $this->output->newLine(); 230 | 231 | $assetsVersion->stampUpdate(); 232 | 233 | if ($run->internal->doneWithFailures() || ! $assetsPublished || ! $packagesDiscovered) { 234 | $this->askToShowErrors($run->internal->exceptions(), $exceptionHandler); 235 | 236 | $this->components->error(ucfirst($this->title()).' occur errors. Run with -v flag to see more details'); 237 | 238 | return self::FAILURE; 239 | } 240 | 241 | $this->registerScheduler($env, $schedule); 242 | 243 | $this->components->info(ucfirst($this->title()).' done!'); 244 | 245 | return self::SUCCESS; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Console/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | exists()) { 17 | $discover->instruction() 18 | ->get($type, Environment::tryFrom($environment))($run); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Console/Commands/SetupCommand.php: -------------------------------------------------------------------------------- 1 | option('force'); 20 | $script = $this->option('script'); 21 | 22 | if ($script) { 23 | $this->addToDumpAutoloadScripts(); 24 | 25 | return self::SUCCESS; 26 | } 27 | 28 | if ($instructions->customExists() && ! $forced) { 29 | $this->components->warn('Setup instructions already exist. Use --force to overwrite.'); 30 | } elseif (($app = $this->getApplication()) instanceof \Symfony\Component\Console\Application) { 31 | $instructions->publish($app); 32 | $this->components->info('Setup instructions published to [routes/setup.php]'); 33 | } 34 | 35 | $this->call('vendor:publish', ['--tag' => 'flora-config', '--force' => $forced]); 36 | 37 | $this->addToDumpAutoloadScripts(); 38 | 39 | return self::SUCCESS; 40 | } 41 | 42 | private function addToDumpAutoloadScripts(): void 43 | { 44 | $composer = json_decode(file_get_contents(base_path('composer.json')), true, 512, JSON_THROW_ON_ERROR); 45 | $scripts = []; 46 | 47 | if (! isset($composer['scripts'])) { 48 | $composer['scripts'] = []; 49 | } 50 | 51 | if (! isset($composer['scripts']['post-autoload-dump'])) { 52 | $composer['scripts']['post-autoload-dump'] = []; 53 | } 54 | 55 | foreach ($composer['scripts']['post-autoload-dump'] as $script) { 56 | $scripts[] = $script === '@php artisan package:discover --ansi' ? '@php artisan update' : $script; 57 | } 58 | 59 | if (! in_array('@php artisan update', $scripts)) { 60 | $scripts[] = '@php artisan update'; 61 | } 62 | 63 | $composer['scripts']['post-autoload-dump'] = $scripts; 64 | 65 | if (isset($composer['scripts']['post-update-cmd'])) { 66 | $scripts = []; 67 | 68 | foreach ($composer['scripts']['post-update-cmd'] as $script) { 69 | if ($script === '@php artisan vendor:publish --tag=laravel-assets --ansi --force') { 70 | continue; 71 | } 72 | 73 | $scripts[] = $script; 74 | } 75 | 76 | if ($scripts === []) { 77 | unset($composer['scripts']['post-update-cmd']); 78 | } else { 79 | $composer['scripts']['post-update-cmd'] = $scripts; 80 | } 81 | } 82 | 83 | file_put_contents(base_path('composer.json'), json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL); 84 | 85 | $this->components->info('[@php artisan update] added to post-autoload-dump scripts'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Console/Commands/UpdateCommand.php: -------------------------------------------------------------------------------- 1 | components->warn((new MissingAppKeyException())->getMessage()); 36 | 37 | if ($this->components->confirm('Run the installation first?', true)) { 38 | return $this->call('install', $this->arguments()); 39 | } 40 | 41 | return self::SUCCESS; 42 | } 43 | 44 | return parent::handle($container, $assetsVersion, $vault, $exceptionHandler, $schedule, $instructions); 45 | } 46 | 47 | protected function title(): string 48 | { 49 | return 'update'; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Console/StopSetupException.php: -------------------------------------------------------------------------------- 1 | has(\Laravel\Horizon\Console\WorkCommand::class); 14 | } 15 | 16 | public function instruction(): Instruction 17 | { 18 | return new Instruction( 19 | update: [ 20 | 'production' => function (Run $run): void { 21 | $run->internal->replace( 22 | fn (Artisan $action): bool => $action->name() === 'queue:restart', 23 | fn (Run $run): Run => $run->command('horizon:terminate'), 24 | ); 25 | }, 26 | ], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Discovers/IdeHelperDiscover.php: -------------------------------------------------------------------------------- 1 | has('command.ide-helper.generate'); 13 | } 14 | 15 | public function instruction(): Instruction 16 | { 17 | return new Instruction( 18 | install: [ 19 | 'local' => static fn (Run $run): Run => $run 20 | ->command('ide-helper:generate') 21 | ->command('ide-helper:meta') 22 | ->command('ide-helper:models', ['--nowrite' => true]) 23 | ->command('ide-helper:eloquent'), 24 | ], 25 | update: [ 26 | 'local' => static fn (Run $run): Run => $run 27 | ->command('ide-helper:generate') 28 | ->command('ide-helper:meta') 29 | ->command('ide-helper:models', ['--nowrite' => true]) 30 | ->command('ide-helper:eloquent'), 31 | ] 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Discovers/Instruction.php: -------------------------------------------------------------------------------- 1 | value; 25 | 26 | if (is_null($this->$typeValue)) { 27 | return static fn () => null; 28 | } 29 | 30 | if (is_array($this->$typeValue)) { 31 | if (isset($this->$typeValue[$environment->value])) { 32 | return $this->$typeValue[$environment->value]; 33 | } 34 | 35 | return static fn () => null; 36 | } 37 | 38 | return $this->$typeValue; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Discovers/PackageDiscover.php: -------------------------------------------------------------------------------- 1 | has('trail:generate'); 13 | } 14 | 15 | public function instruction(): Instruction 16 | { 17 | $command = fn (Run $run): Run => $run->command('trail:generate'); 18 | 19 | return new Instruction( 20 | install: $command, 21 | update: $command, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Discovers/TypeScriptTransformerDiscover.php: -------------------------------------------------------------------------------- 1 | has(\Spatie\TypeScriptTransformer\TypeScriptTransformerConfig::class); 13 | } 14 | 15 | public function instruction(): Instruction 16 | { 17 | $command = fn (Run $run): Run => $run->command('typescript:transform'); 18 | 19 | return new Instruction( 20 | install: $command, 21 | update: $command, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Discovers/VaporUiDiscover.php: -------------------------------------------------------------------------------- 1 | has(\Laravel\VaporUi\Console\PublishCommand::class); 12 | } 13 | 14 | public function instruction(): Instruction 15 | { 16 | return new Instruction( 17 | assetsTag: 'vapor-ui-assets', 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Enums/Environment.php: -------------------------------------------------------------------------------- 1 | name('flora') 26 | ->hasConfigFile() 27 | ->hasCommands( 28 | InstallCommand::class, 29 | UpdateCommand::class, 30 | SetupCommand::class 31 | ); 32 | } 33 | 34 | /** 35 | * Bootstrap the application services. 36 | */ 37 | public function packageBooted(): void 38 | { 39 | $this->createAppMacro(); 40 | 41 | Run::newScript('build', fn (Run $run): Run => $run 42 | ->exec('npm ci') 43 | ->exec('npm run build') 44 | ); 45 | 46 | Run::newScript('cache', fn (Run $run): Run => $run 47 | ->command('route:cache') 48 | ->command('config:cache') 49 | ->command('event:cache') 50 | ); 51 | } 52 | 53 | /** 54 | * Register the application services. 55 | */ 56 | public function packageRegistered(): void 57 | { 58 | $this->app->bind(ChainContract::class, Chain::class); 59 | $this->app->singleton(ChainVaultContract::class, ChainVault::class); 60 | 61 | $this->app->singleton('flora.packages', fn (): array => [ 62 | new VaporUiDiscover(), 63 | new HorizonDiscover(), 64 | new IdeHelperDiscover(), 65 | new TypeScriptTransformerDiscover(), 66 | new TrailDiscover(), 67 | ]); 68 | } 69 | 70 | private function createAppMacro(): void 71 | { 72 | $macros = [ 73 | 'install' => FloraType::Install, 74 | 'update' => FloraType::Update, 75 | ]; 76 | 77 | foreach ($macros as $macro => $type) { 78 | Application::macro( 79 | $macro, 80 | fn (string $environment, callable $callback) => $this->app->make(ChainVaultContract::class)->get($type)->set($environment, $callback) 81 | ); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Run.php: -------------------------------------------------------------------------------- 1 | internal = new RunInternal($this->application, $output, $this); 27 | } 28 | 29 | public static function newScript(string $name, callable $callback): void 30 | { 31 | RunInternal::script($name, $callback); 32 | } 33 | 34 | public function script(string $name, array $arguments = []): static 35 | { 36 | if (! RunInternal::hasScript($name)) { 37 | throw new UndefinedScriptException($name); 38 | } 39 | 40 | $this->internal->push(new Script( 41 | $this->application->getLaravel(), 42 | $this->internal->newRunner(), 43 | $name, 44 | $this->internal->getScript($name), 45 | $arguments, 46 | )); 47 | 48 | return $this; 49 | } 50 | 51 | public function command(string $command, array $parameters = []): static 52 | { 53 | $this->internal->push(new Artisan($this->application, $command, $parameters)); 54 | 55 | return $this; 56 | } 57 | 58 | public function exec(string $command, array $parameters = []): static 59 | { 60 | $this->internal->push(new Process($command, $parameters)); 61 | 62 | return $this; 63 | } 64 | 65 | public function call(callable $callback, array $parameters = [], ?string $name = null): static 66 | { 67 | $this->internal->push(new Callback($this->application->getLaravel(), $callback, $parameters, $name)); 68 | 69 | return $this; 70 | } 71 | 72 | public function job(object|string $job, ?string $queue = null, ?string $connection = null): static 73 | { 74 | $this->internal->push(new Job($job, $queue, $connection)); 75 | 76 | return $this; 77 | } 78 | 79 | public function notify(string $text, string $body, $icon = null): self 80 | { 81 | $this->internal->push(new Notification($this->application->getLaravel(), $text, $body, $icon)); 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/RunInternal.php: -------------------------------------------------------------------------------- 1 | outputComponents = new Factory($output); 41 | } 42 | 43 | public function newRunner() 44 | { 45 | return $this->application->getLaravel()->make(Run::class, [ 46 | 'application' => $this->application, 47 | 'output' => $this->output->isVerbose() ? $this->output : new NullOutput(), 48 | ]); 49 | } 50 | 51 | /** 52 | * Register a custom instruction. 53 | */ 54 | public static function script(string $name, callable $script): void 55 | { 56 | static::$scripts[$name] = $script; 57 | } 58 | 59 | public static function hasScript(string $name): bool 60 | { 61 | return isset(static::$scripts[$name]); 62 | } 63 | 64 | public function getScript(string $name) 65 | { 66 | return static::$scripts[$name]; 67 | } 68 | 69 | public function start(int $labelWidth = 0): void 70 | { 71 | foreach ($this->collection as $action) { 72 | $this->run($action, $labelWidth); 73 | 74 | if ($this->breakOnTerminate && $action->terminated()) { 75 | $this->terminated = true; 76 | break; 77 | } 78 | } 79 | } 80 | 81 | public function rerunLatestAction(): void 82 | { 83 | $this->run($this->latestAction); 84 | } 85 | 86 | public function getCollection(): array 87 | { 88 | return $this->collection; 89 | } 90 | 91 | public function push(Action $action): void 92 | { 93 | $action->withOutput($this->output); 94 | 95 | $this->collection[] = $action; 96 | } 97 | 98 | public function replace(callable $callback, callable $prepare): self|bool 99 | { 100 | $actionType = $this->firstClosureParameterType($callback); 101 | 102 | $keys = collect($this->collection)->filter( 103 | fn ($action): bool => $action instanceof $actionType 104 | )->search($callback); 105 | 106 | if ($keys === false) { 107 | return false; 108 | } 109 | 110 | $replaceAction = is_int($keys) ? $keys : $keys[0]; 111 | 112 | $firstPart = array_slice($this->collection, 0, $replaceAction); 113 | $secondPart = array_slice($this->collection, $replaceAction + 1); 114 | 115 | $this->collection = $firstPart; 116 | 117 | $prepare($this->run); 118 | 119 | $this->collection = array_merge($this->collection, $secondPart); 120 | 121 | return $this; 122 | } 123 | 124 | public function exceptions(): array 125 | { 126 | return $this->exceptions; 127 | } 128 | 129 | public function doneWithFailures(): bool 130 | { 131 | return $this->finishedWithFailures; 132 | } 133 | 134 | public function terminated(): bool 135 | { 136 | return $this->terminated; 137 | } 138 | 139 | public function breakOnTerminate(): self 140 | { 141 | $this->breakOnTerminate = true; 142 | 143 | return $this; 144 | } 145 | 146 | private function run(Action $action, int $labelWidth = 0): self 147 | { 148 | $this->latestAction = $action; 149 | 150 | $internalLabelWidth = collect($this->collection) 151 | ->map(fn (Action $action): string => $action->isSilent() ? '' : $action::$label) 152 | ->reduce(fn ($carry, $label) => max($carry, strlen($label))); 153 | 154 | $action( 155 | $this->outputComponents, 156 | $labelWidth && $labelWidth > $internalLabelWidth ? $labelWidth : $internalLabelWidth 157 | ); 158 | 159 | if ($action->failed()) { 160 | $this->finishedWithFailures = true; 161 | 162 | if (($e = $action->getException()) instanceof \Throwable) { 163 | $this->exceptions[] = [ 164 | 'title' => $action->title(), 165 | 'e' => $e, 166 | ]; 167 | } 168 | } 169 | 170 | return $this; 171 | } 172 | 173 | public function getLatestAction(): ?Action 174 | { 175 | return $this->latestAction; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/SetupInstructions.php: -------------------------------------------------------------------------------- 1 | customFilePath = base_path('routes/setup.php'); 32 | } 33 | 34 | public function customExists(): bool 35 | { 36 | return file_exists($this->customFilePath); 37 | } 38 | 39 | public function load(): void 40 | { 41 | if ($this->customExists()) { 42 | $this->loadCustom(); 43 | 44 | return; 45 | } 46 | 47 | $this->loadDefault(); 48 | } 49 | 50 | public function loadDefault(): void 51 | { 52 | require $this->defaultFilePath; 53 | } 54 | 55 | private function loadCustom(): void 56 | { 57 | require $this->customFilePath; 58 | } 59 | 60 | protected function generateSetupCode( 61 | FloraType $type, 62 | Environment $environment, 63 | Run $run 64 | ): string { 65 | $code = sprintf("App::%s('%s', fn (Run \$run) => \$run", $type->value, $environment->value).PHP_EOL; 66 | $collection = $run->internal->getCollection(); 67 | 68 | foreach ($collection as $item) { 69 | if ($item instanceof Artisan) { 70 | $name = $item->name(); 71 | $parameters = $item->getParameters(); 72 | 73 | $code .= " ->command('$name'".($parameters === [] 74 | ? ')' 75 | : ', '.str(var_export($parameters, true))->replace(PHP_EOL, '')->replace('array ( ', '[')->replace(',)', ']').')'); 76 | } elseif ($item instanceof Process) { 77 | $name = $item->name(); 78 | 79 | $code .= " ->exec('$name')"; 80 | } elseif ($item instanceof Script) { 81 | $name = $item->name(); 82 | 83 | $code .= " ->script('$name')"; 84 | } 85 | 86 | $code .= PHP_EOL; 87 | } 88 | 89 | return $code.');'; 90 | } 91 | 92 | public function publish(Application $application): void 93 | { 94 | $this->loadDefault(); 95 | 96 | $code = Str::before(file_get_contents($this->defaultFilePath), 'App::install'); 97 | 98 | $run = $this->makeRunner($application); 99 | 100 | foreach (FloraType::cases() as $type) { 101 | foreach (Environment::cases() as $env) { 102 | $this->vault->get($type)->get($env->value)($run); 103 | 104 | $this->instructPackages($type, $env->value, $run); 105 | 106 | $code .= $this->generateSetupCode($type, $env, $run).PHP_EOL.PHP_EOL; 107 | 108 | $run = $this->makeRunner($application); 109 | } 110 | } 111 | 112 | file_put_contents($this->customFilePath, Str::beforeLast($code, PHP_EOL)); 113 | } 114 | 115 | private function makeRunner(Application $application): mixed 116 | { 117 | return $this->container->make(Run::class, [ 118 | 'application' => $application, 119 | 'output' => new NullOutput(), 120 | ]); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/UndefinedInstructionException.php: -------------------------------------------------------------------------------- 1 | write("\x1B[1A"); 23 | $output->write("\x1B[2K"); 24 | } 25 | -------------------------------------------------------------------------------- /src/setup.php: -------------------------------------------------------------------------------- 1 | $run 20 | ->command('key:generate') 21 | ->command('migrate') 22 | ->command('storage:link') 23 | ->script('build') 24 | ); 25 | 26 | App::install('production', fn (Run $run) => $run 27 | ->command('key:generate', ['--force' => true]) 28 | ->command('migrate', ['--force' => true]) 29 | ->command('storage:link') 30 | ->script('cache') 31 | ->script('build') 32 | ); 33 | 34 | App::update('local', fn (Run $run) => $run 35 | ->command('migrate') 36 | ->command('cache:clear') 37 | ->script('build') 38 | ); 39 | 40 | App::update('production', fn (Run $run) => $run 41 | ->script('cache') 42 | ->command('migrate', ['--force' => true]) 43 | ->command('cache:clear') 44 | ->command('queue:restart') 45 | ->script('build') 46 | ); 47 | --------------------------------------------------------------------------------