├── LICENSE ├── README.md ├── composer.json ├── config └── deploy-operations.php ├── database └── migrations │ ├── 2022_08_18_180137_change_migration_actions_table.php │ ├── 2023_01_21_172923_rename_migrations_actions_table_to_actions.php │ ├── 2024_05_21_112438_rename_actions_table_to_operations.php │ └── 2024_05_21_114318_rename_column_in_operations_table.php ├── ide.json ├── resources └── stubs │ └── deploy-operation.stub └── src ├── Concerns ├── ConfirmableTrait.php ├── HasAbout.php ├── HasArtisan.php ├── HasIsolatable.php └── HasOptionable.php ├── Console ├── Command.php ├── FreshCommand.php ├── InstallCommand.php ├── MakeCommand.php ├── OperationsCommand.php ├── RollbackCommand.php └── StatusCommand.php ├── Constants ├── Names.php ├── Options.php └── Order.php ├── Data ├── Casts │ ├── BoolCast.php │ ├── Config │ │ ├── ExcludeCast.php │ │ └── PathCast.php │ ├── OperationNameCast.php │ └── PathCast.php ├── Config │ ├── ConfigData.php │ ├── QueueData.php │ ├── ShowData.php │ └── TransactionsData.php └── OptionsData.php ├── Enums ├── MethodEnum.php └── StatusEnum.php ├── Events ├── BaseEvent.php ├── DeployOperationEnded.php ├── DeployOperationFailed.php ├── DeployOperationStarted.php └── NoPendingDeployOperations.php ├── Helpers ├── GitHelper.php ├── OperationHelper.php └── SorterHelper.php ├── Jobs └── OperationJob.php ├── Listeners ├── Listener.php └── MigrationEndedListener.php ├── Notifications └── Notification.php ├── Operation.php ├── Processors ├── FreshProcessor.php ├── InstallProcessor.php ├── MakeProcessor.php ├── OperationsProcessor.php ├── Processor.php ├── RollbackProcessor.php └── StatusProcessor.php ├── Repositories └── OperationsRepository.php ├── ServiceProvider.php ├── Services ├── MigratorService.php └── MutexService.php └── helpers.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Andrey Helldar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Laravel Deploy Operations 2 | 3 | ![the dragon code laravel deploy operations](https://preview.dragon-code.pro/the-dragon-code/deploy-operations.svg?brand=laravel&mode=dark) 4 | 5 | [![Stable Version][badge_stable]][link_packagist] 6 | [![Total Downloads][badge_downloads]][link_packagist] 7 | [![Github Workflow Status][badge_build]][link_build] 8 | [![License][badge_license]][link_license] 9 | 10 | ⚡ **Performing any actions during the deployment process** 11 | 12 | Create specific classes for a one-time or more-time usage, that can be executed automatically after each deployment. 13 | Perfect for seeding or updating some data instantly after some database changes, feature updates, or perform any 14 | actions. 15 | 16 | This package is for you if... 17 | 18 | - you regularly need to update specific data after you deploy new code 19 | - you often perform jobs after deployment 20 | - you sometimes forget to execute that one specific job and stuff gets crazy 21 | - your code gets cluttered with jobs that are not being used anymore 22 | - your co-workers always need to be reminded to execute that one job after some database changes 23 | - you often seed or process data in a migration file (which is a big no-no!) 24 | 25 | ## Installation 26 | 27 | To get the latest version of **Deploy Operations**, simply require the project using [Composer](https://getcomposer.org): 28 | 29 | ```Bash 30 | composer require dragon-code/laravel-deploy-operations 31 | ``` 32 | 33 | ## Documentation 34 | 35 | 📚 [Check out the full documentation to learn everything that Laravel Deploy Operations has to offer.][link_website] 36 | 37 | ## Basic Usage 38 | 39 | Create your first operation using `php artisan make:operation` console command and define the actions it should 40 | perform. 41 | 42 | ```php 43 | use App\Models\Article; 44 | use DragonCode\LaravelDeployOperations\Operation; 45 | 46 | return new class extends Operation { 47 | public function __invoke(): void 48 | { 49 | Article::query() 50 | ->lazyById(chunkSize: 100, column: 'id') 51 | ->each->update(['is_active' => true]); 52 | 53 | // and/or any actions... 54 | } 55 | }; 56 | ``` 57 | 58 | Next, you can run the console command to start operations: 59 | 60 | ```Bash 61 | php artisan operations 62 | ``` 63 | 64 | ## Downloads Stats 65 | 66 | This project has gone the way of several names, and here are the number of downloads of each of them: 67 | 68 | - ![](https://img.shields.io/packagist/dt/dragon-code/laravel-deploy-operations?style=flat-square&label=dragon-code%2Flaravel-deploy-operations) 69 | - ![](https://img.shields.io/packagist/dt/dragon-code/laravel-actions?style=flat-square&label=dragon-code%2Flaravel-actions) 70 | - ![](https://img.shields.io/packagist/dt/dragon-code/laravel-migration-actions?style=flat-square&label=dragon-code%2Flaravel-migration-actions) 71 | - ![](https://img.shields.io/packagist/dt/andrey-helldar/laravel-actions?style=flat-square&label=andrey-helldar%2Flaravel-actions) 72 | 73 | ## License 74 | 75 | This package is licensed under the [MIT License](LICENSE). 76 | 77 | 78 | [badge_build]: https://img.shields.io/github/actions/workflow/status/TheDragonCode/laravel-deploy-operations/tests.yml?style=flat-square 79 | 80 | [badge_downloads]: https://img.shields.io/packagist/dt/dragon-code/laravel-deploy-operations.svg?style=flat-square 81 | 82 | [badge_license]: https://img.shields.io/packagist/l/dragon-code/laravel-deploy-operations.svg?style=flat-square 83 | 84 | [badge_stable]: https://img.shields.io/github/v/release/TheDragonCode/laravel-deploy-operations?label=packagist&style=flat-square 85 | 86 | [link_build]: https://github.com/TheDragonCode/laravel-deploy-operations/actions 87 | 88 | [link_license]: LICENSE 89 | 90 | [link_packagist]: https://packagist.org/packages/dragon-code/laravel-deploy-operations 91 | 92 | [link_website]: https://deploy-operations.dragon-code.pro 93 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dragon-code/laravel-deploy-operations", 3 | "description": "Performing any actions during the deployment process", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "laravel", 8 | "deploy", 9 | "deployment", 10 | "operations", 11 | "action", 12 | "actions", 13 | "migration", 14 | "migrations", 15 | "dragon-code", 16 | "dragon", 17 | "andrey-helldar" 18 | ], 19 | "authors": [ 20 | { 21 | "name": "Andrey Helldar", 22 | "email": "helldar@dragon-code.pro", 23 | "homepage": "https://dragon-code.pro" 24 | } 25 | ], 26 | "support": { 27 | "issues": "https://github.com/TheDragonCode/laravel-actions/issues", 28 | "source": "https://github.com/TheDragonCode/laravel-actions" 29 | }, 30 | "funding": [ 31 | { 32 | "type": "boosty", 33 | "url": "https://boosty.to/dragon-code" 34 | }, 35 | { 36 | "type": "yoomoney", 37 | "url": "https://yoomoney.ru/to/410012608840929" 38 | } 39 | ], 40 | "require": { 41 | "php": "^8.2", 42 | "composer-runtime-api": "^2.2", 43 | "dragon-code/support": "^6.6", 44 | "laravel/framework": "^11.0 || ^12.0", 45 | "laravel/prompts": ">=0.1", 46 | "spatie/laravel-data": "^4.14" 47 | }, 48 | "require-dev": { 49 | "mockery/mockery": "^1.3.1", 50 | "nesbot/carbon": "^2.62.1 || ^3.0", 51 | "orchestra/testbench": "^9.0 || ^10.0", 52 | "phpunit/phpunit": "^11.0 || ^12.0" 53 | }, 54 | "conflict": { 55 | "andrey-helldar/laravel-actions": "*", 56 | "dragon-code/laravel-actions": "*", 57 | "dragon-code/laravel-migration-actions": "*" 58 | }, 59 | "suggest": { 60 | "dragon-code/laravel-data-dumper": "Required if you want to save the execution state using the `schema:dump` console command" 61 | }, 62 | "minimum-stability": "stable", 63 | "prefer-stable": true, 64 | "autoload": { 65 | "psr-4": { 66 | "DragonCode\\LaravelDeployOperations\\": "src/" 67 | }, 68 | "files": [ 69 | "src/helpers.php" 70 | ] 71 | }, 72 | "autoload-dev": { 73 | "psr-4": { 74 | "Tests\\": "tests/" 75 | } 76 | }, 77 | "config": { 78 | "allow-plugins": { 79 | "dragon-code/codestyler": true, 80 | "ergebnis/composer-normalize": true, 81 | "friendsofphp/php-cs-fixer": true, 82 | "symfony/thanks": true 83 | }, 84 | "preferred-install": "dist", 85 | "sort-packages": true 86 | }, 87 | "extra": { 88 | "laravel": { 89 | "providers": [ 90 | "DragonCode\\LaravelDeployOperations\\ServiceProvider" 91 | ] 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /config/deploy-operations.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Operations Repository Table 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This table keeps track of all the operations that have already run for 24 | | your application. Using this information, we can determine which of 25 | | the operations on disk haven't actually been run in the database. 26 | | 27 | */ 28 | 29 | 'table' => 'operations', 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Database Transactions 34 | |-------------------------------------------------------------------------- 35 | | 36 | | This setting defines the rules for working with database transactions. 37 | | This specifies a common value for all operations, but you can override this 38 | | value directly in the class of the operation itself. 39 | */ 40 | 41 | 'transactions' => [ 42 | // Determines whether the use of database transactions is enabled. 43 | 44 | 'enabled' => false, 45 | 46 | // The number of attempts to execute a request within a transaction before throwing an error. 47 | 48 | 'attempts' => 1, 49 | ], 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Operations Path 54 | |-------------------------------------------------------------------------- 55 | | 56 | | This option defines the path to the operation directory. 57 | | 58 | */ 59 | 60 | 'path' => base_path('operations'), 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Path Exclusion 65 | |-------------------------------------------------------------------------- 66 | | 67 | | This option determines which directory and/or file paths should be 68 | | excluded when processing files. 69 | | 70 | | Valid values: array, string or null 71 | | 72 | | Specify `null` to disable. 73 | | 74 | | For example, 75 | | ['foo', 'bar'] 76 | | 'foo' 77 | | null 78 | | 79 | */ 80 | 81 | 'exclude' => null, 82 | 83 | /* 84 | |-------------------------------------------------------------------------- 85 | | Asynchronous settings 86 | |-------------------------------------------------------------------------- 87 | | 88 | | Defines whether the operation will run synchronously or asynchronously. 89 | | 90 | | When this option is activated, each operation will be performed through jobs. 91 | */ 92 | 93 | 'async' => false, 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Queue 98 | |-------------------------------------------------------------------------- 99 | | 100 | | This option specifies the queue settings that will process 101 | | asynchronous operations. 102 | | 103 | */ 104 | 105 | 'queue' => [ 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Queue Connection 109 | |-------------------------------------------------------------------------- 110 | | 111 | | This parameter defines the default connection. 112 | | 113 | */ 114 | 115 | 'connection' => env('DEPLOY_OPERATIONS_QUEUE_CONNECTION', env('QUEUE_CONNECTION', 'sync')), 116 | 117 | /* 118 | |-------------------------------------------------------------------------- 119 | | Queue Name 120 | |-------------------------------------------------------------------------- 121 | | 122 | | This parameter specifies the name of the queue to which asynchronous 123 | | jobs will be sent. 124 | | 125 | */ 126 | 127 | 'name' => env('DEPLOY_OPERATIONS_QUEUE_NAME'), 128 | ], 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | Show 133 | |-------------------------------------------------------------------------- 134 | | 135 | | This option determines the display settings for various information messages. 136 | | 137 | */ 138 | 139 | 'show' => [ 140 | /* 141 | |-------------------------------------------------------------------------- 142 | | Full Path 143 | |-------------------------------------------------------------------------- 144 | | 145 | | This parameter determines how exactly the link to the created file should 146 | | be displayed - the full path to the file or a relative one. 147 | | 148 | */ 149 | 150 | 'full_path' => (bool) env('DEPLOY_OPERATIONS_SHOW_FULL_PATH', false), 151 | ], 152 | ]; 153 | -------------------------------------------------------------------------------- /database/migrations/2022_08_18_180137_change_migration_actions_table.php: -------------------------------------------------------------------------------- 1 | hasTable()) { 14 | Schema::table($this->table(), function (Blueprint $table) { 15 | if ($this->hasColumn('migration') && $this->doesntHaveColumn('action')) { 16 | $table->renameColumn('migration', 'action'); 17 | } 18 | 19 | $table->unsignedInteger('batch')->change(); 20 | }); 21 | } 22 | } 23 | 24 | public function down(): void 25 | { 26 | if ($this->hasTable()) { 27 | Schema::table($this->table(), function (Blueprint $table) { 28 | $table->renameColumn('action', 'migration'); 29 | 30 | $table->integer('batch')->change(); 31 | }); 32 | } 33 | } 34 | 35 | protected function hasTable(): bool 36 | { 37 | return Schema::hasTable($this->table()); 38 | } 39 | 40 | protected function hasColumn(string $column): bool 41 | { 42 | return Schema::hasColumn($this->table(), $column); 43 | } 44 | 45 | protected function doesntHaveColumn(string $column): bool 46 | { 47 | return ! $this->hasColumn($column); 48 | } 49 | 50 | protected function table(): string 51 | { 52 | return app(ConfigData::class)->table; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /database/migrations/2023_01_21_172923_rename_migrations_actions_table_to_actions.php: -------------------------------------------------------------------------------- 1 | doesntSame('migration_actions', $this->table())) { 13 | $this->validateTable($this->table()); 14 | 15 | Schema::rename('migration_actions', $this->table()); 16 | } 17 | } 18 | 19 | public function down(): void 20 | { 21 | if (Schema::hasTable($this->table()) && $this->doesntSame('migration_actions', $this->table())) { 22 | $this->validateTable('migration_actions'); 23 | 24 | Schema::rename($this->table(), 'migration_actions'); 25 | } 26 | } 27 | 28 | protected function validateTable(string $name): void 29 | { 30 | if (Schema::hasTable($name)) { 31 | throw new RuntimeException(sprintf('A table named [%s] already exists. Change the table name settings in the [%s] configuration file.', $name, 'config/actions.php')); 32 | } 33 | } 34 | 35 | protected function doesntSame(string $first, string $second): bool 36 | { 37 | return $first !== $second; 38 | } 39 | 40 | protected function table(): string 41 | { 42 | return app(ConfigData::class)->table; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /database/migrations/2024_05_21_112438_rename_actions_table_to_operations.php: -------------------------------------------------------------------------------- 1 | doesntSame('actions', $this->table())) { 13 | $this->validateTable($this->table()); 14 | 15 | Schema::rename('actions', $this->table()); 16 | } 17 | } 18 | 19 | public function down(): void 20 | { 21 | if (Schema::hasTable($this->table()) && $this->doesntSame('actions', $this->table())) { 22 | $this->validateTable('actions'); 23 | 24 | Schema::rename($this->table(), 'actions'); 25 | } 26 | } 27 | 28 | protected function validateTable(string $name): void 29 | { 30 | if (Schema::hasTable($name)) { 31 | throw new RuntimeException(sprintf('A table named [%s] already exists. Change the table name settings in the [%s] configuration file.', $name, 'config/deploy-operations.php')); 32 | } 33 | } 34 | 35 | protected function doesntSame(string $first, string $second): bool 36 | { 37 | return $first !== $second; 38 | } 39 | 40 | protected function table(): string 41 | { 42 | return app(ConfigData::class)->table; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /database/migrations/2024_05_21_114318_rename_column_in_operations_table.php: -------------------------------------------------------------------------------- 1 | rename('action', 'operation'); 14 | } 15 | 16 | public function down(): void 17 | { 18 | $this->rename('operation', 'action'); 19 | } 20 | 21 | protected function rename(string $from, string $to): void 22 | { 23 | if (Schema::hasColumn($this->table(), $from)) { 24 | Schema::table($this->table(), fn (Blueprint $table) => $table->renameColumn($from, $to)); 25 | } 26 | } 27 | 28 | protected function table(): string 29 | { 30 | return app(ConfigData::class)->table; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /ide.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json", 3 | "codeGenerations": [ 4 | { 5 | "id": "dragon-code.create-deploy-operation", 6 | "name": "Create Deploy Operation", 7 | "inputFilter": "deploy-operations", 8 | "regex": ".+", 9 | "files": [ 10 | { 11 | "directory": "/operations", 12 | "name": "${CURRENT_TIME|format:yyyy_MM_dd_HHmmss}_${INPUT_CLASS|className|replace: ,_|upperCamelCase|snakeCase}.php", 13 | "template": { 14 | "type": "stub", 15 | "path": "/stubs/deploy-operation.stub", 16 | "fallbackPath": "resources/stubs/deploy-operation.stub" 17 | } 18 | } 19 | ] 20 | } 21 | ], 22 | "completions": [ 23 | { 24 | "complete": "directoryFiles", 25 | "condition": [ 26 | { 27 | "classFqn": [ 28 | "DragonCode\\LaravelDeployOperations\\Helpers\\OperationHelper" 29 | ], 30 | "methodNames": [ 31 | "run" 32 | ], 33 | "functionFqn": [ 34 | "DragonCode\\LaravelDeployOperations\\operation" 35 | ], 36 | "place": "parameter", 37 | "parameters": [ 38 | 1 39 | ] 40 | } 41 | ], 42 | "options": { 43 | "directory": "/operations", 44 | "suffixToClear": ".php", 45 | "recursive": true 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /resources/stubs/deploy-operation.stub: -------------------------------------------------------------------------------- 1 | secure || $this->confirmToProceed(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Concerns/HasAbout.php: -------------------------------------------------------------------------------- 1 | getPackageName(), fn () => [ 18 | 'Version' => $this->getPackageVersion(), 19 | ]); 20 | } 21 | 22 | protected function getPackageName(): string 23 | { 24 | return Str::of($this->packageName) 25 | ->after('/') 26 | ->snake() 27 | ->replace('_', ' ') 28 | ->title() 29 | ->toString(); 30 | } 31 | 32 | protected function getPackageVersion(): string 33 | { 34 | return InstalledVersions::getPrettyVersion($this->packageName); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Concerns/HasArtisan.php: -------------------------------------------------------------------------------- 1 | getIsolateOption()) { 23 | return is_numeric($isolate) ? $isolate : self::SUCCESS; 24 | } 25 | 26 | return self::SUCCESS; 27 | } 28 | 29 | protected function getIsolateOption(): bool|int 30 | { 31 | return $this->hasIsolateOption() ? (int) $this->option(Options::Isolated) : false; 32 | } 33 | 34 | protected function hasIsolateOption(): bool 35 | { 36 | return $this->hasOption(Options::Isolated) && $this->option(Options::Isolated); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Concerns/HasOptionable.php: -------------------------------------------------------------------------------- 1 | specifyParameters(); 29 | } 30 | 31 | protected function getOptions(): array 32 | { 33 | return Arr::of($this->availableOptions()) 34 | ->filter(fn (array $option) => in_array($option[0], $this->options, true)) 35 | ->toArray(); 36 | } 37 | 38 | protected function getArguments(): array 39 | { 40 | return Arr::of($this->availableArguments()) 41 | ->filter(fn (array $argument) => in_array($argument[0], $this->arguments, true)) 42 | ->toArray(); 43 | } 44 | 45 | protected function availableArguments(): array 46 | { 47 | return [ 48 | [ 49 | Options::Name, 50 | InputArgument::OPTIONAL, 51 | 'The name of the operation', 52 | ], 53 | ]; 54 | } 55 | 56 | protected function availableOptions(): array 57 | { 58 | return [ 59 | [ 60 | Options::Before, 61 | null, 62 | InputOption::VALUE_NONE, 63 | 'Run operations marked as before', 64 | ], 65 | [ 66 | Options::Connection, 67 | null, 68 | InputOption::VALUE_OPTIONAL, 69 | 'The database connection to use', 70 | ], 71 | [ 72 | Options::Force, 73 | null, 74 | InputOption::VALUE_NONE, 75 | 'Force the operation to run when in production', 76 | ], 77 | [ 78 | Options::Path, 79 | null, 80 | InputOption::VALUE_OPTIONAL, 81 | 'The path to the operations files to be executed', 82 | ], 83 | [ 84 | Options::Realpath, 85 | null, 86 | InputOption::VALUE_NONE, 87 | 'Indicate any provided operation file paths are pre-resolved absolute path', 88 | ], 89 | [ 90 | Options::Step, 91 | null, 92 | InputOption::VALUE_OPTIONAL, 93 | 'Force the operations to be run so they can be rolled back individually', 94 | ], 95 | [ 96 | Options::Mute, 97 | null, 98 | InputOption::VALUE_NONE, 99 | 'Turns off the output of informational messages', 100 | ], 101 | [ 102 | Options::Isolated, 103 | null, 104 | InputOption::VALUE_OPTIONAL, 105 | 'Do not run the operations command if another instance of the operations command is already running', 106 | false, 107 | ], 108 | [ 109 | Options::Sync, 110 | null, 111 | InputOption::VALUE_OPTIONAL, 112 | 'Makes all operations run synchronously', 113 | false, 114 | ], 115 | ]; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Console/Command.php: -------------------------------------------------------------------------------- 1 | allowToProceed()) { 31 | $this->resolveProcessor()->handle(); 32 | 33 | return self::SUCCESS; 34 | } 35 | 36 | return self::FAILURE; 37 | } 38 | 39 | protected function execute(InputInterface $input, OutputInterface $output): int 40 | { 41 | if ($this->getIsolateOption() !== false && ! $this->isolationMutex()->create($this)) { 42 | $this->comment(sprintf('The [%s] command is already running.', $this->getName())); 43 | 44 | return $this->isolatedStatusCode(); 45 | } 46 | 47 | try { 48 | return parent::execute($input, $output); 49 | } 50 | finally { 51 | if ($this->getIsolateOption() !== false) { 52 | $this->isolationMutex()->forget($this); 53 | } 54 | } 55 | } 56 | 57 | protected function resolveProcessor(): Processor 58 | { 59 | return app($this->processor, [ 60 | 'options' => $this->getOptionsData(), 61 | 'input' => $this->input, 62 | 'output' => $this->output, 63 | ]); 64 | } 65 | 66 | protected function getOptionsData(): OptionsData 67 | { 68 | return OptionsData::from(array_merge($this->options(), $this->arguments())); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Console/FreshCommand.php: -------------------------------------------------------------------------------- 1 | map(static fn (string $path) => Str::replace(['\\', '/'], DIRECTORY_SEPARATOR, $path)) 19 | ->filter() 20 | ->all(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Data/Casts/Config/PathCast.php: -------------------------------------------------------------------------------- 1 | replace('\\', '/') 22 | ->replace('.php', '') 23 | ->explode('/') 24 | ->map(fn (string $path) => Str::snake($path)) 25 | ->implode(DIRECTORY_SEPARATOR) 26 | ->toString(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Data/Casts/PathCast.php: -------------------------------------------------------------------------------- 1 | config()->path . $value; 21 | 22 | if ($properties['realpath'] ?? false) { 23 | return $value ?: $path; 24 | } 25 | 26 | return $this->filename($path) ?: $path; 27 | } 28 | 29 | protected function filename(string $path): false|string 30 | { 31 | return realpath(Str::finish($path, '.php')); 32 | } 33 | 34 | protected function config(): ConfigData 35 | { 36 | return app(ConfigData::class); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Data/Config/ConfigData.php: -------------------------------------------------------------------------------- 1 | 'Ran', 17 | self::Pending => 'Pending', 18 | self::Skipped => 'Skipped', 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Events/BaseEvent.php: -------------------------------------------------------------------------------- 1 | hasGitDirectory($path)) { 20 | return $this->exec('rev-parse --abbrev-ref HEAD', $this->resolvePath($path)); 21 | } 22 | 23 | return null; 24 | } 25 | 26 | protected function exec(string $command, ?string $path = null): ?string 27 | { 28 | return exec(sprintf('git -C "%s" %s', $path, $command)); 29 | } 30 | 31 | protected function hasGitDirectory(?string $path = null): bool 32 | { 33 | if ($path = rtrim($this->resolvePath($path), '/\\')) { 34 | return Directory::exists($path . DIRECTORY_SEPARATOR . '.git'); 35 | } 36 | 37 | return false; 38 | } 39 | 40 | protected function resolvePath(?string $path = null): string 41 | { 42 | return realpath($path ?: base_path()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Helpers/OperationHelper.php: -------------------------------------------------------------------------------- 1 | when($path, fn (Collection $items) => $items->put('--' . Options::Path, $path)) 18 | ->when($realpath, fn (Collection $items) => $items->put('--' . Options::Realpath, true)) 19 | ->all(); 20 | 21 | Artisan::call(OperationsCommand::class, $parameters); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Helpers/SorterHelper.php: -------------------------------------------------------------------------------- 1 | callback()); 18 | } 19 | 20 | public function byKeys(array $items): array 21 | { 22 | return Arr::ksort($items, $this->callback()); 23 | } 24 | 25 | public function byRan(array $values, array $completed): array 26 | { 27 | foreach ($values as $value) { 28 | if (! in_array($value, $completed, true)) { 29 | $completed[] = $value; 30 | } 31 | } 32 | 33 | return $completed; 34 | } 35 | 36 | protected function callback(): Closure 37 | { 38 | return static function (string $a, string $b): int { 39 | $current = Path::filename($a); 40 | $next = Path::filename($b); 41 | 42 | if ($current === $next) { 43 | return 0; 44 | } 45 | 46 | return $current < $next ? -1 : 1; 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Jobs/OperationJob.php: -------------------------------------------------------------------------------- 1 | onConnection($this->config()->queue->connection); 31 | $this->onQueue($this->config()->queue->name); 32 | } 33 | 34 | public function handle(): void 35 | { 36 | Artisan::call(Names::Operations, [ 37 | '--' . Options::Path => $this->filename, 38 | '--' . Options::Sync => true, 39 | ]); 40 | } 41 | 42 | public function uniqueId(): string 43 | { 44 | return $this->filename; 45 | } 46 | 47 | protected function config(): ConfigData 48 | { 49 | return app(ConfigData::class); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Listeners/Listener.php: -------------------------------------------------------------------------------- 1 | withOperation(); 21 | } 22 | 23 | return null; 24 | } 25 | 26 | protected function run(string $method, string $operation): void 27 | { 28 | match ($method) { 29 | 'up' => $this->call(OperationsCommand::class, $operation), 30 | 'down' => $this->call(RollbackCommand::class, $operation, ['--force' => true]), 31 | }; 32 | } 33 | 34 | protected function call(string $command, string $filename, array $parameters = []): void 35 | { 36 | Artisan::call($command, array_merge([ 37 | '--' . Options::Path => $filename, 38 | ], $parameters)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Listeners/MigrationEndedListener.php: -------------------------------------------------------------------------------- 1 | withOperation($event->migration)) { 14 | $this->run($event->method, $operation); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Notifications/Notification.php: -------------------------------------------------------------------------------- 1 | canSpeak()) { 25 | $this->components()->line($style, $string, $this->verbosity); 26 | } 27 | } 28 | 29 | public function info(string $string): void 30 | { 31 | if ($this->canSpeak()) { 32 | $this->components()->info($string, $this->verbosity); 33 | } 34 | } 35 | 36 | public function warning(string $string): void 37 | { 38 | if ($this->canSpeak()) { 39 | $this->components()->warn($string, $this->verbosity); 40 | } 41 | } 42 | 43 | public function task(string $description, Closure $task): void 44 | { 45 | if ($this->canSpeak()) { 46 | $this->components()->task($description, $task); 47 | 48 | return; 49 | } 50 | 51 | $task(); 52 | } 53 | 54 | public function twoColumn(string $first, string $second): void 55 | { 56 | if ($this->canSpeak()) { 57 | $this->components()->twoColumnDetail($first, $second, $this->verbosity); 58 | } 59 | } 60 | 61 | protected function components(): Factory 62 | { 63 | return $this->components ??= new Factory($this->output); 64 | } 65 | 66 | public function setOutput(OutputStyle $output, bool $silent = false): Notification 67 | { 68 | $this->output = $output; 69 | $this->silent = $silent; 70 | 71 | return $this; 72 | } 73 | 74 | protected function canSpeak(): bool 75 | { 76 | return ! $this->silent; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Operation.php: -------------------------------------------------------------------------------- 1 | transactions->enabled; 33 | } 34 | 35 | /** 36 | * Determines whether the given operation can be called conditionally. 37 | */ 38 | public function shouldRun(): bool 39 | { 40 | return true; 41 | } 42 | 43 | /** 44 | * Defines a possible "pre-launch" of the operation. 45 | */ 46 | public function needBefore(): bool 47 | { 48 | return true; 49 | } 50 | 51 | /** 52 | * Defines whether the operation will run synchronously or asynchronously. 53 | */ 54 | public function needAsync(): bool 55 | { 56 | return app(ConfigData::class)->async; 57 | } 58 | 59 | /** 60 | * Method to be called when the job completes successfully. 61 | */ 62 | public function success(): void {} 63 | 64 | /** 65 | * The method will be called if an error occurs. 66 | */ 67 | public function failed(): void {} 68 | } 69 | -------------------------------------------------------------------------------- /src/Processors/FreshProcessor.php: -------------------------------------------------------------------------------- 1 | drop(); 15 | $this->operations(); 16 | } 17 | 18 | protected function drop(): void 19 | { 20 | if ($this->repository->repositoryExists()) { 21 | $this->notification->task('Dropping all operations', fn () => $this->repository->deleteRepository()); 22 | } 23 | } 24 | 25 | protected function operations(): void 26 | { 27 | $this->runCommand(Names::Operations, [ 28 | '--' . Options::Connection => $this->options->connection, 29 | '--' . Options::Path => $this->options->path, 30 | '--' . Options::Realpath => true, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Processors/InstallProcessor.php: -------------------------------------------------------------------------------- 1 | exists()) { 16 | $this->notification->info('Operations repository already exists'); 17 | 18 | return; 19 | } 20 | 21 | $this->notification->task('Installing the operation repository', function () { 22 | $this->create(); 23 | $this->ensureDirectory(); 24 | }); 25 | } 26 | 27 | protected function isFile(string $path): bool 28 | { 29 | return Str::of($path)->lower()->endsWith('.php'); 30 | } 31 | 32 | protected function exists(): bool 33 | { 34 | return $this->repository->repositoryExists(); 35 | } 36 | 37 | protected function create(): void 38 | { 39 | $this->repository->createRepository(); 40 | } 41 | 42 | protected function ensureDirectory(): void 43 | { 44 | $this->isFile($this->options->path) 45 | ? Directory::ensureDirectory(Path::dirname($this->options->path)) 46 | : Directory::ensureDirectory($this->options->path); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Processors/MakeProcessor.php: -------------------------------------------------------------------------------- 1 | getFullPath(); 26 | 27 | $this->notification->task($this->message($fullPath), fn () => $this->create($fullPath)); 28 | } 29 | 30 | protected function message(string $path): string 31 | { 32 | return 'Operation [' . $this->displayName($path) . '] created successfully'; 33 | } 34 | 35 | protected function create(string $path): void 36 | { 37 | File::copy($this->stubPath(), $path); 38 | } 39 | 40 | protected function displayName(string $path): string 41 | { 42 | return Str::of($path) 43 | ->when(! $this->showFullPath(), fn (Stringable $str) => $str->after(base_path())) 44 | ->replace('\\', '/') 45 | ->ltrim('./') 46 | ->toString(); 47 | } 48 | 49 | protected function getName(): string 50 | { 51 | return $this->getFilename( 52 | $this->getBranchName() 53 | ); 54 | } 55 | 56 | protected function getPath(): string 57 | { 58 | return $this->options->path; 59 | } 60 | 61 | protected function getFullPath(): string 62 | { 63 | return $this->getPath() . $this->getName(); 64 | } 65 | 66 | protected function getFilename(string $branch): string 67 | { 68 | $directory = Path::dirname($branch); 69 | $filename = Path::filename($branch); 70 | 71 | return Str::of($filename) 72 | ->snake() 73 | ->prepend($this->getTime()) 74 | ->finish('.php') 75 | ->prepend($directory . '/') 76 | ->replace('\\', '/') 77 | ->ltrim('./') 78 | ->toString(); 79 | } 80 | 81 | protected function getBranchName(): string 82 | { 83 | if ($name = trim((string) $this->options->name)) { 84 | return $name; 85 | } 86 | 87 | if ($name = $this->askForName()) { 88 | return $name; 89 | } 90 | 91 | return $this->git->currentBranch() ?? $this->fallback; 92 | } 93 | 94 | protected function askForName(): string 95 | { 96 | $prompt = $this->promptForName(); 97 | 98 | return text($prompt[0], $prompt[1], hint: $prompt[2]); 99 | } 100 | 101 | protected function promptForName(): array 102 | { 103 | return ['What should the operation be named?', 'E.g. activate articles', 'Press Enter to autodetect']; 104 | } 105 | 106 | protected function getTime(): string 107 | { 108 | return date('Y_m_d_His_'); 109 | } 110 | 111 | protected function stubPath(): string 112 | { 113 | if ($path = realpath(base_path('stubs/deploy-operation.stub'))) { 114 | return $path; 115 | } 116 | 117 | return $this->defaultStub; 118 | } 119 | 120 | protected function showFullPath(): bool 121 | { 122 | return $this->config->show->fullPath; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Processors/OperationsProcessor.php: -------------------------------------------------------------------------------- 1 | showCaption(); 22 | $this->ensureRepository(); 23 | $this->runOperations($this->getCompleted()); 24 | } 25 | 26 | protected function showCaption(): void 27 | { 28 | $this->notification->info('Running operations'); 29 | } 30 | 31 | protected function ensureRepository(): void 32 | { 33 | $this->runCommand(Names::Install, [ 34 | '--' . Options::Connection => $this->options->connection, 35 | '--' . Options::Force => true, 36 | '--' . Options::Mute => true, 37 | ]); 38 | } 39 | 40 | protected function runOperations(array $completed): void 41 | { 42 | try { 43 | if ($files = $this->getNewFiles($completed)) { 44 | $this->fireEvent(DeployOperationStarted::class, MethodEnum::Up); 45 | 46 | $this->runEach($files, $this->getBatch()); 47 | 48 | $this->fireEvent(DeployOperationEnded::class, MethodEnum::Up); 49 | 50 | return; 51 | } 52 | 53 | $this->fireEvent(NoPendingDeployOperations::class, MethodEnum::Up); 54 | } 55 | catch (Throwable $e) { 56 | $this->fireEvent(DeployOperationFailed::class, MethodEnum::Up); 57 | 58 | throw $e; 59 | } 60 | } 61 | 62 | protected function runEach(array $files, int $batch): void 63 | { 64 | foreach ($files as $file) { 65 | $this->run($file, $batch); 66 | } 67 | } 68 | 69 | protected function run(string $filename, int $batch): void 70 | { 71 | $this->migrator->runUp($filename, $batch, $this->options); 72 | } 73 | 74 | protected function getNewFiles(array $completed): array 75 | { 76 | return $this->getFiles( 77 | path: $this->options->path, 78 | filter: fn (string $file) => ! Str::of($file)->replace('\\', '/')->contains($completed) 79 | ); 80 | } 81 | 82 | protected function getCompleted(): array 83 | { 84 | return $this->repository->getCompleted()->pluck('operation')->all(); 85 | } 86 | 87 | protected function getBatch(): int 88 | { 89 | return $this->repository->getNextBatchNumber(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Processors/Processor.php: -------------------------------------------------------------------------------- 1 | notification->setOutput($this->output, $this->options->mute); 46 | $this->repository->setConnection($this->options->connection); 47 | $this->migrator->setConnection($this->options->connection)->setOutput($this->output); 48 | } 49 | 50 | protected function getFiles(string $path, ?Closure $filter = null): array 51 | { 52 | $file = Str::finish($path, '.php'); 53 | 54 | $files = $this->isFile($file) ? [$file] : $this->file->names($path, $filter, true); 55 | 56 | $files = Arr::filter( 57 | $files, 58 | fn (string $path) => Str::endsWith($path, '.php') && ! Str::contains($path, $this->config->exclude) 59 | ); 60 | 61 | return Arr::of($this->sorter->byValues($files)) 62 | ->map(fn (string $value) => Str::before($value, '.php')) 63 | ->toArray(); 64 | } 65 | 66 | protected function runCommand(string $command, array $options = []): void 67 | { 68 | $this->artisan($command, array_filter($options), $this->output); 69 | } 70 | 71 | protected function tableNotFound(): bool 72 | { 73 | if (! $this->repository->repositoryExists()) { 74 | $this->notification->warning('Deploy operations table not found'); 75 | 76 | return true; 77 | } 78 | 79 | return false; 80 | } 81 | 82 | protected function fireEvent(string $event, MethodEnum $method): void 83 | { 84 | $this->events->dispatch(new $event($method, $this->options->before)); 85 | } 86 | 87 | protected function isFile(string $path): bool 88 | { 89 | return $this->file->exists($path) && $this->file->isFile($path); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Processors/RollbackProcessor.php: -------------------------------------------------------------------------------- 1 | tableNotFound() || $this->nothingToRollback()) { 17 | $this->fireEvent(NoPendingDeployOperations::class, MethodEnum::Down); 18 | 19 | return; 20 | } 21 | 22 | if ($items = $this->getOperations($this->options->step)) { 23 | $this->fireEvent(DeployOperationStarted::class, MethodEnum::Down); 24 | 25 | $this->showCaption(); 26 | $this->run($items); 27 | 28 | $this->fireEvent(DeployOperationEnded::class, MethodEnum::Down); 29 | 30 | return; 31 | } 32 | 33 | $this->fireEvent(NoPendingDeployOperations::class, MethodEnum::Down); 34 | } 35 | 36 | protected function showCaption(): void 37 | { 38 | $this->notification->info('Rollback Operations'); 39 | } 40 | 41 | protected function run(array $rows): void 42 | { 43 | foreach ($rows as $row) { 44 | $this->rollback($row->operation); 45 | } 46 | } 47 | 48 | protected function getOperations(?int $step): array 49 | { 50 | return (int) $step > 0 51 | ? $this->repository->getByStep($step) 52 | : $this->repository->getLast(); 53 | } 54 | 55 | protected function rollback(string $item): void 56 | { 57 | $this->migrator->runDown($item, $this->options); 58 | } 59 | 60 | protected function nothingToRollback(): bool 61 | { 62 | if ($this->count() <= 0) { 63 | $this->notification->info('Nothing To Rollback'); 64 | 65 | return true; 66 | } 67 | 68 | return false; 69 | } 70 | 71 | protected function count(): int 72 | { 73 | return $this->repository->getLastBatchNumber(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Processors/StatusProcessor.php: -------------------------------------------------------------------------------- 1 | Operation name'; 15 | 16 | protected string $columnStatus = 'Batch / Status'; 17 | 18 | public function handle(): void 19 | { 20 | if ($this->tableNotFound()) { 21 | return; 22 | } 23 | 24 | [$files, $completed] = $this->getData(); 25 | 26 | if ($this->isEmpty($files, $completed)) { 27 | $this->notification->info('No operations found'); 28 | 29 | return; 30 | } 31 | 32 | $this->showCaption(); 33 | $this->showHeaders(); 34 | $this->showStatus($files, $completed); 35 | } 36 | 37 | protected function showCaption(): void 38 | { 39 | $this->notification->info('Show Status'); 40 | } 41 | 42 | protected function showHeaders(): void 43 | { 44 | $this->notification->twoColumn($this->columnName, $this->columnStatus); 45 | } 46 | 47 | protected function showStatus(array $items, array $completed): void 48 | { 49 | foreach ($this->merge($items, array_keys($completed)) as $item) { 50 | $status = $this->getStatusFor($completed, $item); 51 | 52 | $this->notification->twoColumn($item, $status); 53 | } 54 | } 55 | 56 | protected function merge(array $items, array $completed): array 57 | { 58 | return $this->sorter->byRan($items, $completed); 59 | } 60 | 61 | protected function getData(): array 62 | { 63 | $files = $this->getFiles($this->options->path); 64 | $completed = $this->getCompleted(); 65 | 66 | return [$files, $completed]; 67 | } 68 | 69 | protected function getStatusFor(array $completed, string $item): string 70 | { 71 | if ($batch = Arr::get($completed, $item)) { 72 | return sprintf('[%s] %s', $batch, StatusEnum::Ran->toColor()); 73 | } 74 | 75 | return StatusEnum::Pending->toColor(); 76 | } 77 | 78 | protected function getCompleted(): array 79 | { 80 | return $this->repository->getCompleted() 81 | ->pluck('batch', 'operation') 82 | ->all(); 83 | } 84 | 85 | protected function isEmpty(array $items, array $completed): bool 86 | { 87 | return empty($items) && empty($completed); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Repositories/OperationsRepository.php: -------------------------------------------------------------------------------- 1 | sortedTable()->get(); 30 | } 31 | 32 | public function getByStep(int $steps): array 33 | { 34 | return $this->sortedTable(Order::Desc) 35 | ->whereIn('batch', $this->getBatchNumbers($steps)) 36 | ->get() 37 | ->all(); 38 | } 39 | 40 | public function getLast(): array 41 | { 42 | return $this->sortedTable(Order::Desc) 43 | ->where('batch', $this->getLastBatchNumber()) 44 | ->get() 45 | ->all(); 46 | } 47 | 48 | public function getNextBatchNumber(): int 49 | { 50 | return $this->getLastBatchNumber() + 1; 51 | } 52 | 53 | public function getLastBatchNumber(): int 54 | { 55 | return (int) $this->table()->max('batch'); 56 | } 57 | 58 | public function log(string $operation, int $batch): void 59 | { 60 | $this->table()->insert(compact('operation', 'batch')); 61 | } 62 | 63 | public function delete(string $operation): void 64 | { 65 | $this->table()->where(compact('operation'))->delete(); 66 | } 67 | 68 | public function createRepository(): void 69 | { 70 | $this->schema()->create($this->config->table, function (Blueprint $table) { 71 | $table->bigIncrements('id'); 72 | 73 | $table->string('operation'); 74 | 75 | $table->unsignedInteger('batch'); 76 | }); 77 | } 78 | 79 | public function repositoryExists(): bool 80 | { 81 | return $this->schema()->hasTable($this->config->table); 82 | } 83 | 84 | public function deleteRepository(): void 85 | { 86 | $this->schema()->dropIfExists($this->config->table); 87 | } 88 | 89 | /** 90 | * @return array 91 | */ 92 | protected function getBatchNumbers(int $steps): array 93 | { 94 | return $this->sortedTable(Order::Desc) 95 | ->pluck('batch') 96 | ->unique() 97 | ->take($steps) 98 | ->all(); 99 | } 100 | 101 | protected function sortedTable(string $order = Order::Asc): Query 102 | { 103 | return $this->table() 104 | ->orderBy('batch', $order) 105 | ->orderBy('id', $order); 106 | } 107 | 108 | protected function schema(): Builder 109 | { 110 | return $this->getConnection()->getSchemaBuilder(); 111 | } 112 | 113 | protected function table(): Query 114 | { 115 | return $this->getConnection()->table($this->config->table)->useWritePdo(); 116 | } 117 | 118 | protected function getConnection(): ConnectionInterface 119 | { 120 | return $this->resolver->connection( 121 | $this->connection ?: $this->config->connection 122 | ); 123 | } 124 | 125 | public function setConnection(?string $connection): self 126 | { 127 | $this->connection = $connection; 128 | 129 | return $this; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerEvents(); 23 | $this->bootConfig(); 24 | 25 | if ($this->app->runningInConsole()) { 26 | $this->publishConfig(); 27 | $this->publishStub(); 28 | 29 | $this->registerAbout(); 30 | $this->registerCommands(); 31 | $this->registerMigrations(); 32 | } 33 | } 34 | 35 | public function register(): void 36 | { 37 | $this->registerConfig(); 38 | } 39 | 40 | protected function registerCommands(): void 41 | { 42 | $this->commands([ 43 | Console\OperationsCommand::class, 44 | Console\FreshCommand::class, 45 | Console\InstallCommand::class, 46 | Console\MakeCommand::class, 47 | Console\RollbackCommand::class, 48 | Console\StatusCommand::class, 49 | ]); 50 | } 51 | 52 | protected function registerEvents(): void 53 | { 54 | Event::listen(MigrationEnded::class, MigrationEndedListener::class); 55 | } 56 | 57 | protected function registerMigrations(): void 58 | { 59 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 60 | } 61 | 62 | protected function publishConfig(): void 63 | { 64 | $this->publishes([ 65 | __DIR__ . '/../config/deploy-operations.php' => $this->app->configPath('deploy-operations.php'), 66 | ], ['config', 'deploy-operations']); 67 | } 68 | 69 | protected function publishStub(): void 70 | { 71 | $this->publishes([ 72 | __DIR__ . '/../resources/stubs/deploy-operation.stub' => $this->app->basePath( 73 | 'stubs/deploy-operation.stub' 74 | ), 75 | ], ['stubs', 'deploy-operations']); 76 | } 77 | 78 | protected function registerConfig(): void 79 | { 80 | $this->mergeConfigFrom(__DIR__ . '/../config/deploy-operations.php', 'deploy-operations'); 81 | } 82 | 83 | protected function bootConfig(): void 84 | { 85 | $this->app->bind(ConfigData::class, static fn () => ConfigData::from( 86 | config('deploy-operations') 87 | )); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Services/MigratorService.php: -------------------------------------------------------------------------------- 1 | repository->setConnection($connection); 40 | 41 | return $this; 42 | } 43 | 44 | public function setOutput(OutputStyle $output): self 45 | { 46 | $this->notification->setOutput($output); 47 | 48 | return $this; 49 | } 50 | 51 | public function runUp(string $filename, int $batch, OptionsData $options): void 52 | { 53 | $path = $this->resolvePath($filename, $options->path); 54 | $operation = $this->resolveOperation($path); 55 | $name = $this->resolveOperationName($path); 56 | 57 | if (! $this->allowOperation($operation, $options)) { 58 | $this->notification->twoColumn($name, StatusEnum::Skipped->toColor()); 59 | 60 | return; 61 | } 62 | 63 | if ($this->needAsync($operation, $options)) { 64 | OperationJob::dispatch($name); 65 | 66 | $this->notification->twoColumn($name, StatusEnum::Pending->toColor()); 67 | 68 | return; 69 | } 70 | 71 | $this->notification->task($name, function () use ($operation, $name, $batch) { 72 | $this->hasMethod($operation, '__invoke') 73 | ? $this->runOperation($operation, '__invoke') 74 | : $this->runOperation($operation, 'up'); 75 | 76 | if ($operation->shouldOnce()) { 77 | $this->log($name, $batch); 78 | } 79 | }); 80 | } 81 | 82 | public function runDown(string $filename, OptionsData $options): void 83 | { 84 | $path = $this->resolvePath($filename, $options->path); 85 | $operation = $this->resolveOperation($path); 86 | $name = $this->resolveOperationName($path); 87 | 88 | $this->notification->task($name, function () use ($operation, $name) { 89 | $this->runOperation($operation, 'down'); 90 | $this->deleteLog($name); 91 | }); 92 | } 93 | 94 | protected function runOperation(Operation $operation, string $method): void 95 | { 96 | if ($this->hasMethod($operation, $method)) { 97 | try { 98 | $this->runMethod($operation, $method, $operation->withinTransactions()); 99 | 100 | $operation->success(); 101 | } 102 | catch (Throwable $e) { 103 | $operation->failed(); 104 | 105 | throw $e; 106 | } 107 | } 108 | } 109 | 110 | protected function hasMethod(Operation $operation, string $method): bool 111 | { 112 | return method_exists($operation, $method); 113 | } 114 | 115 | protected function needAsync(Operation $operation, OptionsData $options): bool 116 | { 117 | return ! $options->sync && $operation->needAsync(); 118 | } 119 | 120 | protected function runMethod(Operation $operation, string $method, bool $transactions): void 121 | { 122 | $callback = fn () => $this->container->call([$operation, $method]); 123 | 124 | $transactions ? DB::transaction($callback, $this->config->transactions->attempts) : $callback(); 125 | } 126 | 127 | protected function log(string $name, int $batch): void 128 | { 129 | $this->repository->log($name, $batch); 130 | } 131 | 132 | protected function deleteLog(string $name): void 133 | { 134 | $this->repository->delete($name); 135 | } 136 | 137 | protected function allowOperation(Operation $operation, OptionsData $options): bool 138 | { 139 | if (! $operation->shouldRun()) { 140 | return false; 141 | } 142 | 143 | return ! $this->disallowBefore($operation, $options); 144 | } 145 | 146 | protected function disallowBefore(Operation $operation, OptionsData $options): bool 147 | { 148 | return $options->before && ! $operation->needBefore(); 149 | } 150 | 151 | protected function resolvePath(string $filename, string $path): string 152 | { 153 | if (file_exists($path) && is_file($path)) { 154 | return $path; 155 | } 156 | 157 | $withExtension = Str::finish($filename, '.php'); 158 | 159 | if ($this->file->exists($withExtension) && $this->file->isFile($withExtension)) { 160 | return $withExtension; 161 | } 162 | 163 | return Str::finish($path . DIRECTORY_SEPARATOR . $filename, '.php'); 164 | } 165 | 166 | protected function resolveOperation(string $path): Operation 167 | { 168 | if ($this->file->exists($path)) { 169 | return require $path; 170 | } 171 | 172 | throw new FileNotFoundException($path); 173 | } 174 | 175 | protected function resolveOperationName(string $path): string 176 | { 177 | return Str::of(realpath($path)) 178 | ->after(realpath($this->config->path) . DIRECTORY_SEPARATOR) 179 | ->replace(['\\', '/'], '/') 180 | ->before('.php') 181 | ->toString(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Services/MutexService.php: -------------------------------------------------------------------------------- 1 | store()->add($this->name($command), true, $this->ttl()); 23 | } 24 | 25 | public function forget(Command $command): void 26 | { 27 | $this->store()->forget( 28 | $this->name($command) 29 | ); 30 | } 31 | 32 | protected function store(): Repository 33 | { 34 | return $this->cache->store($this->store); 35 | } 36 | 37 | protected function ttl(): CarbonInterval 38 | { 39 | return CarbonInterval::hour(); 40 | } 41 | 42 | protected function name(Command $command): string 43 | { 44 | return 'framework' . DIRECTORY_SEPARATOR . 'deploy-operation-' . $command->getName(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |