diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..3ba6281 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: MoamenEltouny +buy_me_a_coffee: moameneltouny \ No newline at end of file diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml new file mode 100644 index 0000000..5a596f1 --- /dev/null +++ b/.github/workflows/php-cs-fixer.yml @@ -0,0 +1,23 @@ +name: PHP Coding Standards Fixer + +on: [push] + +jobs: + php-cs-fixer: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Run PHP CS Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php-cs-fixer.dist.php --allow-risky=yes + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Fixing php-cs \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/tests.yml similarity index 74% rename from .github/workflows/build.yml rename to .github/workflows/tests.yml index 5a014ea..789cb38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,9 @@ -name: build +name: Tests -on: [push, pull_request] +on: + push: + branches: [ main, develop ] + pull_request: jobs: run: @@ -8,7 +11,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-versions: ["8.1", "8.2", "8.3"] + php-versions: ['8.2', '8.3', '8.4'] name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} steps: @@ -19,7 +22,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: mbstring, pdo, pdo_sqlite, sqlite3, intl, zip + extensions: mbstring, pdo, intl, zip coverage: none - name: Check PHP Version @@ -35,7 +38,7 @@ jobs: run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest + run: composer install --prefer-dist --no-progress - name: Run test suite - run: vendor/bin/phpunit + run: vendor/bin/phpunit --testdox \ No newline at end of file diff --git a/.gitignore b/.gitignore index 35024a0..929319b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -vendor +/vendor composer.lock -.phpunit.cache \ No newline at end of file +.phpunit.result.cache +.php-cs-fixer.cache \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..70e2ea4 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,39 @@ +notPath('bootstrap/*') + ->notPath('storage/*') + ->notPath('storage/*') + ->notPath('resources/view/mail/*') + ->in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'indentation_type' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + ]) + ->setFinder($finder); diff --git a/README.md b/README.md index 9bbdbe8..db0108c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@

- Laravel Version : 10.x, 11.x - License - GitHub Actions Workflow Status -
- Source - Packagist Version - Packagist Downloads +Build Status +Laravel Version : 11.x + Total Downloads +Latest Stable Version +License

Allows you to execute commands, actions, jobs, and automated tasks on your production server.

diff --git a/composer.json b/composer.json index 79f4465..1a26986 100644 --- a/composer.json +++ b/composer.json @@ -24,11 +24,12 @@ } ], "require": { - "php": ">=8.1", - "laravel/framework": ">=10.0" + "php": "~8.2|~8.3|~8.4", + "laravel/framework": "^11.0", + "pharaonic/php-dot-array": "^2.0" }, "require-dev": { - "orchestra/testbench": "^8.0" + "orchestra/testbench": "^9.15" }, "config": { "sort-packages": true diff --git a/config/executor.php b/config/executor.php new file mode 100644 index 0000000..f8defa3 --- /dev/null +++ b/config/executor.php @@ -0,0 +1,13 @@ + env('EXECUTOR_CONNECTION', config('database.default')), + + /** + * The table that should be used to store the executors. + */ + 'table' => env('EXECUTOR_TABLE', 'executors'), +]; diff --git a/database/migrations/2024_07_07_000001_create_executors_table.php b/database/migrations/2024_07_07_000001_create_executors_table.php index 23ad719..ee4b7a9 100644 --- a/database/migrations/2024_07_07_000001_create_executors_table.php +++ b/database/migrations/2024_07_07_000001_create_executors_table.php @@ -14,15 +14,17 @@ class CreateExecutorsTable extends Migration */ public function up() { - Schema::create('executors', function (Blueprint $table) { - $table->id(); - $table->unsignedTinyInteger('type')->default(ExecutorType::Always); - $table->string('executor'); - $table->string('tag')->nullable(); - $table->integer('batch')->default(1); - $table->integer('executed')->default(0); - $table->timestamp('last_executed_at')->nullable(); - }); + Schema::connection(config('pharaonic.executor.connection', config('database.default'))) + ->create(config('pharaonic.executor.table', 'executors'), function (Blueprint $table) { + $table->id(); + $table->unsignedTinyInteger('type')->default(ExecutorType::Always); + $table->string('name'); + $table->json('tags')->nullable(); + $table->json('servers')->nullable(); + $table->integer('batch')->nullable(); + $table->integer('executed')->default(0); + $table->timestamp('last_executed_at')->nullable(); + }); } /** @@ -32,6 +34,7 @@ public function up() */ public function down() { - Schema::dropIfExists('executors'); + Schema::connection(config('pharaonic.executor.connection', config('database.default'))) + ->dropIfExists(config('pharaonic.executor.table', 'executors')); } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5d2e220..84e9176 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,13 @@ - - - - ./tests/ - - - - - ./src/ - - + + + + + tests + + + diff --git a/src/Executor.php b/src/Classes/Executor.php similarity index 60% rename from src/Executor.php rename to src/Classes/Executor.php index b504264..bfa00f4 100644 --- a/src/Executor.php +++ b/src/Classes/Executor.php @@ -1,15 +1,24 @@ type instanceof ExecutorType)) { + if (! ($this->type instanceof ExecutorType)) { throw new \Exception('The type of the executor must be an instance of ExecutorType.'); } - if (!is_null($this->tag) && !is_string($this->tag) && !is_array($this->tag)) { - throw new \Exception('The tag of the executor must be a string or an array or null.'); + if (! is_array($this->tags)) { + throw new \Exception('The tags of the executor must be an array.'); + } + + if (! is_array($this->servers)) { + throw new \Exception('The servers of the executor must be an array of ips.'); } $this->output = new OutputStyle(new ArgvInput(), new ConsoleOutput()); diff --git a/src/Classes/ExecutorItem.php b/src/Classes/ExecutorItem.php new file mode 100644 index 0000000..6a4930d --- /dev/null +++ b/src/Classes/ExecutorItem.php @@ -0,0 +1,128 @@ +executor = $executor; + $this->file = $file; + $this->name = $name; + $this->model = $model; + } + + /** + * Get info about the executor. + * + * @return array + */ + public function info() + { + return [ + 'name' => $this->name, + 'path' => $this->file->getRealPath(), + 'type' => $this->executor->type, + 'tags' => $this->executor->tags ?: null, + 'servers' => $this->executor->servers ?: null, + 'batch' => $this->model?->batch, + 'executed' => $this->model?->executed > 0, + ]; + } + + /** + * Determine if the executor is executable. + * + * @return bool + */ + public function isExecutable() + { + if (! $this->model) { + return true; + } + + if ($servers = $this->executor->servers ?: null) { + if (! array_intersect($servers, ExecutorFacade::getIPs())) { + return false; + } + } + + $this->model?->fill([ + 'type' => $this->executor->type, + 'tags' => $this->executor->tags, + 'servers' => $this->executor->servers, + ]); + + return $this->model?->isExecutable() ?? true; + } + + /** + * Run the executor. + * + * @return void + */ + public function run(int $nextBatch) + { + $this->executor->up(); + + if (! $this->model) { + $this->model = Model::create([ + 'type' => $this->executor->type, + 'name' => $this->name, + 'tags' => $this->executor->tags, + 'batch' => $nextBatch, + 'executed' => 1, + 'last_executed_at' => now(), + ]); + } else { + $this->model->executed += 1; + $this->model->last_executed_at = now(); + $this->model->save(); + } + } + + /** + * Rollback the executor. + * + * @return void + */ + public function rollback() + { + $this->executor->down(); + } +} diff --git a/src/Classes/ExecutorManager.php b/src/Classes/ExecutorManager.php new file mode 100644 index 0000000..5a6627f --- /dev/null +++ b/src/Classes/ExecutorManager.php @@ -0,0 +1,86 @@ +pool = new ExecutorPool(); + } + + /** + * Get the executor pool instance. + * + * @return ExecutorPool + */ + public function getPool() + { + return $this->pool; + } + + /** + * Get all executor records. + * + * @return \Illuminate\Support\Collection + */ + public function getRecords() + { + return Executor::all()->keyBy('name'); + } + + /** + * Get info about all executors. + * + * @return array + */ + public function info() + { + return $this->pool + ->collect($this->getRecords()) + ->info(); + } + + /** + * Get the next batch number. + * + * @return int + */ + public function getNextBatchNumber() + { + return (Executor::orderBy('batch', 'desc')->first()?->batch ?? 0) + 1; + } + + /** + * Get the server IPs. + * + * @return array + */ + public function getIPs() + { + if (! empty($this->ips)) { + return $this->ips; + } + + return $this->ips = array_filter( + dot(net_get_interfaces())->get('*.unicast.*.address'), + fn ($ip) => ! in_array($ip, ['127.0.0.1', '::1', null]) + ); + } +} diff --git a/src/Classes/ExecutorPool.php b/src/Classes/ExecutorPool.php new file mode 100644 index 0000000..d9d06da --- /dev/null +++ b/src/Classes/ExecutorPool.php @@ -0,0 +1,111 @@ +paths = [base_path('executors')]; + } + + /** + * Add a new path to executors pools. + * + * @param string $path + * @return static + */ + public function addPath(string $path) + { + array_push($this->paths, $path); + + return $this; + } + + /** + * Return all pools of executors. + * + * @return array + */ + public function getPaths(): array + { + return $this->paths; + } + + /** + * Get info about all executors. + * + * @return array + */ + public function info() + { + return collect($this->items)->map(function ($item) { + return $item->info(); + })->toArray(); + } + + /** + * Collect all executors from the defined paths. + * + * @param Collection $records + * @return static + */ + public function collect(Collection $records) + { + if (! empty($this->items)) { + $this->items = []; + } + + foreach ($this->paths as $path) { + if (File::isDirectory($path) && ! File::isEmptyDirectory($path, true)) { + foreach (File::files($path) as $file) { + if ($file->getExtension() === 'php') { + $obj = include $file->getRealPath(); + + if (! $obj instanceof Executor) { + continue; + } + + $name = basename($file->getFileName(), '.php'); + $record = $records->get($name); + + if (isset($this->items[$name])) { + throw new \Exception("Duplicate Executor name [$name] in file: " . $file->getRealPath()); + } + + $this->items[$name] = new ExecutorItem($obj, $file, $name, $record); + } + } + } + } + + return $this; + } + + /** + * Get all collected executors items. + * + * @return array + */ + public function getItems() + { + return $this->items; + } +} diff --git a/src/Classes/ExecutorPoolClass.php b/src/Classes/ExecutorPoolClass.php deleted file mode 100644 index d390051..0000000 --- a/src/Classes/ExecutorPoolClass.php +++ /dev/null @@ -1,39 +0,0 @@ -paths = [base_path('executors')]; - } - - /** - * Return all pools of executors. - * - * @return array - */ - public function getPaths(): array - { - return $this->paths; - } - - /** - * Add a new path to executors pools. - * - * @param string $path - * @return void - */ - public function addPath(string $path): void - { - array_push($this->paths, $path); - } -} diff --git a/src/Console/ExecuteCommand.php b/src/Console/ExecuteCommand.php index b8db1e5..abdf8f1 100644 --- a/src/Console/ExecuteCommand.php +++ b/src/Console/ExecuteCommand.php @@ -3,7 +3,7 @@ namespace Pharaonic\Laravel\Executor\Console; use Illuminate\Console\Command; -use Pharaonic\Laravel\Executor\Services\ExecutorService; +use Pharaonic\Laravel\Executor\Facades\Executor as ExecutorFacade; class ExecuteCommand extends Command { @@ -13,7 +13,7 @@ class ExecuteCommand extends Command * @var string */ protected $signature = 'execute {name?} - {--tag= : Execute the executor with the specified tag}'; + {--tags= : Execute the executor with the specified tags}'; /** * The console command description. @@ -25,31 +25,41 @@ class ExecuteCommand extends Command /** * Execute the console command. */ - public function handle(ExecutorService $service) + public function handle() { - if (!$service->isExists()) { - $this->warn('There are no executors need to be executed.'); - } + $items = ExecutorFacade::getPool() + ->collect(ExecutorFacade::getRecords()) + ->getItems(); - $list = $service->sync(); $name = $this->argument('name'); - $tag = $this->option('tag'); + $tags = $this->option('tags'); + $toRun = []; - $executors = $list - ->filter(fn ($executor) => $executor['model']->executable) - ->when($name, fn ($executors) => $executors->where('name', $name)) - ->when($tag, fn ($executors) => $executors->where('tag', $tag)); + foreach ($items as $item) { + if ($name && $item->name != $name) { + continue; + } - if ($executors->isEmpty()) { - $this->warn('There are no executors need to be executed.'); + if ($tags && ! array_intersect(explode(',', $tags), $item->tags)) { + continue; + } + + if ($item->isExecutable()) { + $toRun[] = $item; + } } - $executors->each(function ($executor) { - $this->info("Executing {$executor['name']}..."); + if (empty($toRun)) { + $this->warn('There are no executors need to be executed.'); + } else { + $batch = ExecutorFacade::getNextBatchNumber(); + + foreach ($toRun as $item) { + $this->info("Executing {$item->name}..."); - (include $executor['path'])->handle(); - $executor['model']->execute(); - }); + $item->run($batch); + } + } return 0; } diff --git a/src/Console/ExecuteFreshCommand.php b/src/Console/ExecuteFreshCommand.php index a5f3f88..6cf9909 100644 --- a/src/Console/ExecuteFreshCommand.php +++ b/src/Console/ExecuteFreshCommand.php @@ -23,7 +23,7 @@ class ExecuteFreshCommand extends Command /** * Execute the console command. - * + * * @return int */ public function handle() diff --git a/src/Console/ExecuteMakeCommand.php b/src/Console/ExecuteMakeCommand.php index c0194ad..b6c462d 100644 --- a/src/Console/ExecuteMakeCommand.php +++ b/src/Console/ExecuteMakeCommand.php @@ -14,8 +14,6 @@ class ExecuteMakeCommand extends GeneratorCommand * @var string */ protected $signature = 'execute:make {name} - {--o|once : Create a new executor class that will be executed once} - {--tag= : The tag of the executor} {--path= : The path of the executor}'; /** @@ -42,81 +40,6 @@ protected function getStub() return __DIR__ . '/../../stubs/executor.php.stub'; } - /** - * Replace the class name for the given stub. - * - * @param string $stub - * @param string $name - * @return string - */ - protected function replaceClass($stub, $name) - { - $stub = parent::replaceClass($stub, $name); - - $this->setTypeProperty($stub); - $this->setTagProperty($stub); - - return $stub; - } - - /** - * Set the type property for the executor. - * - * @param string $stub - * @return void - */ - protected function setTypeProperty(string &$stub) - { - if (!$this->option('once')) { - $stub = str_replace( - [ - "{{ type }}\n", - "{{ type-use }}\n" - ], - '', - $stub - ); - - return; - } - - $stub = str_replace( - [ - '{{ type-use }}', - '{{ type }}', - ], - [ - 'use Pharaonic\Laravel\Executor\Enums\ExecutorType;', - file_get_contents(__DIR__ . '/../../stubs/property/once.stub') . PHP_EOL, - ], - $stub - ); - } - - /** - * Replace the once option for the given stub. - * - * @param string $stub - * @return void - */ - protected function setTagProperty(string &$stub) - { - if (!$this->option('tag')) { - $stub = str_replace("{{ tag }}\n", '', $stub); - return; - } - - $stub = str_replace( - '{{ tag }}', - str_replace( - '{{ tag }}', - '"' . $this->option('tag') . '"', - file_get_contents(__DIR__ . '/../../stubs/property/tag.stub') - ) . PHP_EOL, - $stub - ); - } - /** * Get the destination class path. * diff --git a/src/Console/ExecuteRollbackCommand.php b/src/Console/ExecuteRollbackCommand.php index 6c0a55d..bac453d 100644 --- a/src/Console/ExecuteRollbackCommand.php +++ b/src/Console/ExecuteRollbackCommand.php @@ -3,6 +3,7 @@ namespace Pharaonic\Laravel\Executor\Console; use Illuminate\Console\Command; +use Pharaonic\Laravel\Executor\Facades\Executor as ExecutorFacade; use Pharaonic\Laravel\Executor\Models\Executor; class ExecuteRollbackCommand extends Command @@ -20,11 +21,11 @@ class ExecuteRollbackCommand extends Command * * @var string */ - protected $description = 'Rollback the lastest executors that has been inserted.'; + protected $description = 'Rollback the latest executors that have been inserted.'; /** * Execute the console command. - * + * * @return int */ public function handle() @@ -32,13 +33,26 @@ public function handle() $batches = Executor::orderBy('batch', 'desc')->groupBy('batch')->limit($this->option('steps'))->pluck('batch')->toArray(); if (empty($batches)) { - $this->error('There are no executors has been found.'); - return 1; + $this->warn('There are no executors has been found.'); + + return 0; + } + + $items = ExecutorFacade::getPool() + ->collect(ExecutorFacade::getRecords()) + ->getItems(); + + foreach ($items as $item) { + if ($item->model && in_array($item->model->batch, $batches)) { + $this->info("Rolling back {$item->name}..."); + + $item->rollback(); + } } Executor::whereIn('batch', $batches)->delete(); - $this->info('Executors has been rollbacked successfully.'); + $this->info('Executors has been rollback successfully.'); return 0; } diff --git a/src/Console/ExecuteStatusCommand.php b/src/Console/ExecuteStatusCommand.php index cb34e70..b16106d 100644 --- a/src/Console/ExecuteStatusCommand.php +++ b/src/Console/ExecuteStatusCommand.php @@ -3,7 +3,7 @@ namespace Pharaonic\Laravel\Executor\Console; use Illuminate\Console\Command; -use Pharaonic\Laravel\Executor\Services\ExecutorService; +use Pharaonic\Laravel\Executor\Facades\Executor; class ExecuteStatusCommand extends Command { @@ -24,19 +24,18 @@ class ExecuteStatusCommand extends Command /** * Execute the console command. */ - public function handle(ExecutorService $service) + public function handle() { - $executors = $service->sync(); - $this->table( - ['Name', 'Type', 'Tag', 'Batch', 'Executed'], - $executors->map(function ($executor) { + ['Name', 'Type', 'Tags', 'Servers', 'Batch', 'Executed'], + collect(Executor::info())->map(function ($executor) { return [ $executor['name'], - ucfirst($executor['type']->name), - $executor['tag'], - $executor['model']->batch, - $executor['model']->executed > 0 ? 'Yes' : 'No', + $executor['type']->name, + empty($executor['tags']) ? 'None' : implode(', ', $executor['tags']), + empty($executor['servers']) ? 'All' : implode(', ', $executor['servers']), + $executor['batch'] ?? 'N/A', + $executor['executed'] ? 'Yes' : 'No', ]; }) ); diff --git a/src/Enums/ExecutorType.php b/src/Enums/ExecutorType.php index 2ee201f..bdd0db1 100644 --- a/src/Enums/ExecutorType.php +++ b/src/Enums/ExecutorType.php @@ -14,7 +14,7 @@ enum ExecutorType: int /** * Check if the executor will be executed always. * - * @return boolean + * @return bool */ public function isAlways(): bool { @@ -24,7 +24,7 @@ public function isAlways(): bool /** * Check if the executor will be executed once. * - * @return boolean + * @return bool */ public function isOnce(): bool { diff --git a/src/ExecutorServiceProvider.php b/src/ExecutorServiceProvider.php index efba8c2..492ef78 100644 --- a/src/ExecutorServiceProvider.php +++ b/src/ExecutorServiceProvider.php @@ -2,8 +2,9 @@ namespace Pharaonic\Laravel\Executor; +use Illuminate\Foundation\Console\AboutCommand; use Illuminate\Support\ServiceProvider; -use Pharaonic\Laravel\Executor\Classes\ExecutorPoolClass; +use Pharaonic\Laravel\Executor\Classes\ExecutorManager; use Pharaonic\Laravel\Executor\Console\ExecuteCommand; use Pharaonic\Laravel\Executor\Console\ExecuteFreshCommand; use Pharaonic\Laravel\Executor\Console\ExecuteMakeCommand; @@ -19,7 +20,10 @@ class ExecutorServiceProvider extends ServiceProvider */ public function register() { - $this->app->singleton('pharaonic.executor.executorPool', ExecutorPoolClass::class); + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + $this->mergeConfigFrom(__DIR__ . '/../config/executor.php', 'pharaonic.executor'); + + $this->app->singleton('pharaonic.executor.manager', ExecutorManager::class); } /** @@ -30,16 +34,18 @@ public function register() public function boot() { if ($this->app->runningInConsole()) { - // Publish Migrations + AboutCommand::add('Pharaonic', ['Executor' => '11.0.0']); + $this->publishes( [__DIR__ . '/../database/migrations' => database_path('migrations')], - ['pharaonic', 'migrations', 'executor-migrations'] + ['pharaonic', 'migrations', 'laravel-executor'] ); - // Load Migrations - $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + $this->publishes( + [__DIR__ . '/../config/executor.php' => config_path('pharaonic/executor.php')], + ['pharaonic', 'config', 'laravel-executor'] + ); - // Load Commands $this->commands([ ExecuteCommand::class, ExecuteMakeCommand::class, diff --git a/src/Facades/Executor.php b/src/Facades/Executor.php new file mode 100644 index 0000000..0a27749 --- /dev/null +++ b/src/Facades/Executor.php @@ -0,0 +1,20 @@ + ExecutorType::class, + 'tags' => 'array', 'batch' => 'integer', 'executed' => 'integer', 'last_executed_at' => 'datetime', ]; /** - * Get the executable attribute. + * Get the current connection name for the model. * - * @return void + * @return string|null + */ + public function getConnectionName() + { + return config('pharaonic.executor.connection', parent::getConnectionName()); + } + + /** + * Get the table associated with the model. + * + * @return string + */ + public function getTable() + { + return config('pharaonic.executor.table', parent::getTable()); + } + + /** + * Check if the executor is new (never executed). + * + * @return bool + */ + public function isNew() + { + return $this->executed === 0; + } + + /** + * Check if the executor is executable. + * + * @return bool */ - public function getExecutableAttribute() + public function isExecutable() { - return $this->type->isAlways() || ($this->type->isOnce() && $this->executed == 0); + return $this->type->isAlways() || $this->isNew(); } /** diff --git a/src/Services/ExecutorService.php b/src/Services/ExecutorService.php deleted file mode 100644 index 8b23727..0000000 --- a/src/Services/ExecutorService.php +++ /dev/null @@ -1,122 +0,0 @@ -dir = base_path('executors'); - } - - /** - * Check if the executors directory exists. - * - * @return bool - */ - public function isExists() - { - return is_dir($this->dir); - } - - /** - * Get the executors. - * - * @return \Illuminate\Support\Collection - */ - protected function prepareExecutors() - { - $db = Executor::all()->keyBy('executor'); - - return collect(array_map(function ($file) use ($db) { - $class = new ReflectionClass(include $file); - $name = basename($class->getFileName(), '.php'); - $executor = [ - 'name' => $name, - 'type' => $class->getProperty('type')->getDefaultValue(), - 'tag' => $class->getProperty('tag')->getDefaultValue(), - 'path' => $class->getFileName(), - 'model' => $db[$name] ?? null - ]; - - if ($executor['model']) { - $executor['model']->fill([ - 'type' => $executor['type'], - 'tag' => $executor['tag'], - ]); - - if ($executor['model']->isDirty()) { - $executor['model']->save(); - } - } - return $executor; - }, $this->getPaths()))->keyBy('name'); - } - - - /** - * Sync the executors. - * - * @return \Illuminate\Support\Collection - */ - public function sync() - { - $executors = $this->prepareExecutors(); - - $newExecutors = $executors->filter(fn ($executor) => $executor['model'] == null); - if ($newExecutors->isNotEmpty()) { - $batch = $this->getNextBatch(); - - $newExecutors->each(function ($executor) use (&$executors, $batch) { - $executor['model'] = new Executor([ - 'executor' => $executor['name'], - 'type' => $executor['type'], - 'tag' => $executor['tag'], - 'batch' => $batch, - ]); - - $executors->offsetSet($executor['name'], $executor); - }); - } - - return $executors; - } - - /** - * Get the next batch number. - * - * @return int - */ - protected function getNextBatch() - { - return (Executor::orderBy('batch', 'desc')->first()?->batch ?? 0) + 1; - } - - /** - * Get the paths of the executors. - * - * @return array - */ - protected function getPaths() - { - $collectPath = collect([]); - - foreach (ExecutorPool::getPaths() as $path) { - $collectPath = $collectPath->merge(File::glob($path . '/*')); - } - - return $collectPath->all(); - } -} diff --git a/src/Traits/InteractsWithIO.php b/src/Traits/InteractsWithIO.php deleted file mode 100644 index 2755c14..0000000 --- a/src/Traits/InteractsWithIO.php +++ /dev/null @@ -1,331 +0,0 @@ - OutputInterface::VERBOSITY_VERBOSE, - 'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE, - 'vvv' => OutputInterface::VERBOSITY_DEBUG, - 'quiet' => OutputInterface::VERBOSITY_QUIET, - 'normal' => OutputInterface::VERBOSITY_NORMAL, - ]; - - /** - * Confirm a question with the user. - * - * @param string $question - * @param bool $default - * @return bool - */ - public function confirm($question, $default = false) - { - return $this->output->confirm($question, $default); - } - - /** - * Prompt the user for input. - * - * @param string $question - * @param string|null $default - * @return mixed - */ - public function ask($question, $default = null) - { - return $this->output->ask($question, $default); - } - - /** - * Prompt the user for input with auto completion. - * - * @param string $question - * @param array|callable $choices - * @param string|null $default - * @return mixed - */ - public function anticipate($question, $choices, $default = null) - { - return $this->askWithCompletion($question, $choices, $default); - } - - /** - * Prompt the user for input with auto completion. - * - * @param string $question - * @param array|callable $choices - * @param string|null $default - * @return mixed - */ - public function askWithCompletion($question, $choices, $default = null) - { - $question = new Question($question, $default); - - is_callable($choices) - ? $question->setAutocompleterCallback($choices) - : $question->setAutocompleterValues($choices); - - return $this->output->askQuestion($question); - } - - /** - * Prompt the user for input but hide the answer from the console. - * - * @param string $question - * @param bool $fallback - * @return mixed - */ - public function secret($question, $fallback = true) - { - $question = new Question($question); - - $question->setHidden(true)->setHiddenFallback($fallback); - - return $this->output->askQuestion($question); - } - - /** - * Give the user a single choice from an array of answers. - * - * @param string $question - * @param array $choices - * @param string|int|null $default - * @param mixed|null $attempts - * @param bool $multiple - * @return string|array - */ - public function choice($question, array $choices, $default = null, $attempts = null, $multiple = false) - { - $question = new ChoiceQuestion($question, $choices, $default); - - $question->setMaxAttempts($attempts)->setMultiselect($multiple); - - return $this->output->askQuestion($question); - } - - /** - * Format input to textual table. - * - * @param array $headers - * @param \Illuminate\Contracts\Support\Arrayable|array $rows - * @param \Symfony\Component\Console\Helper\TableStyle|string $tableStyle - * @param array $columnStyles - * @return void - */ - public function table($headers, $rows, $tableStyle = 'default', array $columnStyles = []) - { - $table = new Table($this->output); - - if ($rows instanceof Arrayable) { - $rows = $rows->toArray(); - } - - $table->setHeaders((array) $headers)->setRows($rows)->setStyle($tableStyle); - - foreach ($columnStyles as $columnIndex => $columnStyle) { - $table->setColumnStyle($columnIndex, $columnStyle); - } - - $table->render(); - } - - /** - * Execute a given callback while advancing a progress bar. - * - * @param iterable|int $totalSteps - * @param \Closure $callback - * @return mixed|void - */ - public function withProgressBar($totalSteps, Closure $callback) - { - $bar = $this->output->createProgressBar( - is_iterable($totalSteps) ? count($totalSteps) : $totalSteps - ); - - $bar->start(); - - if (is_iterable($totalSteps)) { - foreach ($totalSteps as $value) { - $callback($value, $bar); - - $bar->advance(); - } - } else { - $callback($bar); - } - - $bar->finish(); - - if (is_iterable($totalSteps)) { - return $totalSteps; - } - } - - /** - * Write a string as information output. - * - * @param string $string - * @param int|string|null $verbosity - * @return void - */ - public function info($string, $verbosity = null) - { - $this->line($string, 'info', $verbosity); - } - - /** - * Write a string as standard output. - * - * @param string $string - * @param string|null $style - * @param int|string|null $verbosity - * @return void - */ - public function line($string, $style = null, $verbosity = null) - { - $styled = $style ? "<$style>$string" : $string; - - $this->output->writeln($styled, $this->parseVerbosity($verbosity)); - } - - /** - * Write a string as comment output. - * - * @param string $string - * @param int|string|null $verbosity - * @return void - */ - public function comment($string, $verbosity = null) - { - $this->line($string, 'comment', $verbosity); - } - - /** - * Write a string as question output. - * - * @param string $string - * @param int|string|null $verbosity - * @return void - */ - public function question($string, $verbosity = null) - { - $this->line($string, 'question', $verbosity); - } - - /** - * Write a string as error output. - * - * @param string $string - * @param int|string|null $verbosity - * @return void - */ - public function error($string, $verbosity = null) - { - $this->line($string, 'error', $verbosity); - } - - /** - * Write a string as warning output. - * - * @param string $string - * @param int|string|null $verbosity - * @return void - */ - public function warn($string, $verbosity = null) - { - if (! $this->output->getFormatter()->hasStyle('warning')) { - $style = new OutputFormatterStyle('yellow'); - - $this->output->getFormatter()->setStyle('warning', $style); - } - - $this->line($string, 'warning', $verbosity); - } - - /** - * Write a string in an alert box. - * - * @param string $string - * @param int|string|null $verbosity - * @return void - */ - public function alert($string, $verbosity = null) - { - $length = Str::length(strip_tags($string)) + 12; - - $this->comment(str_repeat('*', $length), $verbosity); - $this->comment('* '.$string.' *', $verbosity); - $this->comment(str_repeat('*', $length), $verbosity); - - $this->comment('', $verbosity); - } - - /** - * Write a blank line. - * - * @param int $count - * @return $this - */ - public function newLine($count = 1) - { - $this->output->newLine($count); - - return $this; - } - - /** - * Set the verbosity level. - * - * @param string|int $level - * @return void - */ - protected function setVerbosity($level) - { - $this->verbosity = $this->parseVerbosity($level); - } - - /** - * Get the verbosity level in terms of Symfony's OutputInterface level. - * - * @param string|int|null $level - * @return int - */ - protected function parseVerbosity($level = null) - { - if (isset($this->verbosityMap[$level])) { - $level = $this->verbosityMap[$level]; - } elseif (! is_int($level)) { - $level = $this->verbosity; - } - - return $level; - } -} diff --git a/stubs/executor.php.stub b/stubs/executor.php.stub index 9c51a4a..4b09d23 100644 --- a/stubs/executor.php.stub +++ b/stubs/executor.php.stub @@ -1,19 +1,47 @@ assertExitCode(0); } - public function testMakeOnceExecutor() - { - $this->artisan('execute:make', [ - 'name' => 'testMakeOnceExecutor', - '--once' => true, - ]) - ->assertExitCode(0); - } - - public function testMakeExecutorWithTag() - { - $this->artisan('execute:make', [ - 'name' => 'testMakeExecutorWithTag', - '--tag' => 'test', - ]) - ->assertExitCode(0); - } - - public function testMakeOnceExecutorWithTag() - { - $this->artisan('execute:make', [ - 'name' => 'testMakeOnceExecutorWithTag', - '--once' => true, - '--tag' => 'test', - ]) - ->assertExitCode(0); - } - public function testExecute() { - $this->testMakeExecutor(); + $this->artisan('execute:make', ['name' => 'testMakeExecutor']); $this->artisan('execute')->assertOk(); $this->assertEquals(1, Executor::count()); } public function testRollbackSuccess() { - $this->testMakeExecutor(); + $this->artisan('execute:make', ['name' => 'testMakeExecutor']); $this->artisan('execute')->assertOk(); $this->artisan('execute:rollback')->assertOk(); $this->assertEquals(0, Executor::count()); @@ -61,35 +33,35 @@ public function testRollbackSuccess() public function testRollbackFailed() { - $this->artisan('execute:rollback')->assertFailed(); + $this->artisan('execute:rollback') + ->expectsOutput('There are no executors has been found.') + ->assertExitCode(0); } public function testFreshExecutors() { - $this->testMakeExecutor(); + $this->artisan('execute:make', ['name' => 'testMakeExecutor']); $this->artisan('execute:fresh')->assertOk(); $this->assertEquals(1, Executor::count()); } public function testStatusOfExecutors() { - $this->testMakeExecutor(); - - $executors = (new ExecutorService)->sync(); - + $this->artisan('execute:make', ['name' => 'testMakeExecutor']); $this->artisan('execute:status') ->assertOk() ->expectsTable( - ['Name', 'Type', 'Tag', 'Batch', 'Executed'], - $executors->map(function ($executor) { - return [ - $executor['name'], - ucfirst($executor['type']->name), - $executor['tag'], - $executor['model']->batch, - $executor['model']->executed > 0 ? 'Yes' : 'No', - ]; - }) + ['Name', 'Type', 'Tags', 'Servers', 'Batch', 'Executed'], + [ + [ + basename(File::files(base_path('executors'))[0]->getBasename(), '.php'), + 'Always', + 'None', + 'All', + 'N/A', + 'No', + ], + ] ); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 8998d59..820e273 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -34,11 +34,11 @@ protected function getPackageProviders($app) */ protected function getEnvironmentSetUp($app) { - $app['config']->set('database.default', 'testbench'); - $app['config']->set('database.connections.testbench', [ - 'driver' => 'sqlite', + $app['config']->set('database.default', 'testing'); + $app['config']->set('database.connections.testing', [ + 'driver' => 'sqlite', 'database' => ':memory:', - 'prefix' => '', + 'prefix' => '', ]); } }